Files
TimeTrack/routes/company.py
Jens Luedicke 9a79778ad6 Squashed commit of the following:
commit 1eeea9f83ad9230a5c1f7a75662770eaab0df837
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 21:15:41 2025 +0200

    Disable resuming of old time entries.

commit 3e3ec2f01cb7943622b819a19179388078ae1315
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 20:59:19 2025 +0200

    Refactor db migrations.

commit 15a51a569da36c6b7c9e01ab17b6fdbdee6ad994
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 19:58:04 2025 +0200

    Apply new style for Time Tracking view.

commit 77e5278b303e060d2b03853b06277f8aa567ae68
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 18:06:04 2025 +0200

    Allow direct registrations as a Company.

commit 188a8772757cbef374243d3a5f29e4440ddecabe
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 18:04:45 2025 +0200

    Add email invitation feature.

commit d9ebaa02aa01b518960a20dccdd5a327d82f30c6
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 17:12:32 2025 +0200

    Apply common style for Company, User, Team management pages.

commit 81149caf4d8fc6317e2ab1b4f022b32fc5aa6d22
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 16:44:32 2025 +0200

    Move export functions to own module.

commit 1a26e19338e73f8849c671471dd15cc3c1b1fe82
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 15:51:15 2025 +0200

    Split up models.py.

commit 61f1ccd10f721b0ff4dc1eccf30c7a1ee13f204d
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 12:05:28 2025 +0200

    Move utility function into own modules.

commit 84b341ed35e2c5387819a8b9f9d41eca900ae79f
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 11:44:24 2025 +0200

    Refactor auth functions use.

commit 923e311e3da5b26d85845c2832b73b7b17c48adb
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 11:35:52 2025 +0200

    Refactor route nameing and fix bugs along the way.

commit f0a5c4419c340e62a2615c60b2a9de28204d2995
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 10:34:33 2025 +0200

    Fix URL endpoints in announcement template.

commit b74d74542a1c8dc350749e4788a9464d067a88b5
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 09:25:53 2025 +0200

    Move announcements to own module.

commit 9563a28021ac46c82c04fe4649b394dbf96f92c7
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 09:16:30 2025 +0200

    Combine Company view and edit templates.

commit 6687c373e681d54e4deab6b2582fed5cea9aadf6
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 08:17:42 2025 +0200

    Move Users, Company and System Administration to own modules.

commit 8b7894a2e3eb84bb059f546648b6b9536fea724e
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 07:40:57 2025 +0200

    Move Teams and Projects to own modules.

commit d11bf059d99839ecf1f5d7020b8c8c8a2454c00b
Author: Jens Luedicke <jens@luedicke.me>
Date:   Mon Jul 7 07:09:33 2025 +0200

    Move Tasks and Sprints to own modules.
2025-07-07 21:16:36 +02:00

360 lines
17 KiB
Python

"""
Company management routes
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, g, session
from models import db, Company, User, Role, Team, Project, SystemSettings, CompanyWorkConfig, WorkRegion
from routes.auth import admin_required, company_required, login_required
import logging
import re
logger = logging.getLogger(__name__)
companies_bp = Blueprint('companies', __name__, url_prefix='/admin/company')
@companies_bp.route('', methods=['GET', 'POST'])
@admin_required
@company_required
def admin_company():
"""View and manage company settings"""
company = g.company
# Handle form submissions
if request.method == 'POST':
action = request.form.get('action')
if action == 'update_company_details':
# Handle company details update
name = request.form.get('name')
description = request.form.get('description', '')
max_users = request.form.get('max_users')
is_active = 'is_active' in request.form
# Validate input
error = None
if not name:
error = 'Company name is required'
elif name != company.name and Company.query.filter_by(name=name).first():
error = 'Company name already exists'
if max_users:
try:
max_users = int(max_users)
if max_users < 1:
error = 'Maximum users must be at least 1'
except ValueError:
error = 'Maximum users must be a valid number'
else:
max_users = None
if error is None:
company.name = name
company.description = description
company.max_users = max_users
company.is_active = is_active
db.session.commit()
flash('Company details updated successfully!', 'success')
else:
flash(error, 'error')
return redirect(url_for('companies.admin_company'))
elif action == 'update_system_settings':
# Update registration setting
registration_enabled = 'registration_enabled' in request.form
reg_setting = SystemSettings.query.filter_by(key='registration_enabled').first()
if reg_setting:
reg_setting.value = 'true' if registration_enabled else 'false'
# Update email verification setting
email_verification_required = 'email_verification_required' in request.form
email_setting = SystemSettings.query.filter_by(key='email_verification_required').first()
if email_setting:
email_setting.value = 'true' if email_verification_required else 'false'
db.session.commit()
flash('System settings updated successfully!', 'success')
return redirect(url_for('companies.admin_company'))
elif action == 'update_work_policies':
# Get or create company work config
work_config = CompanyWorkConfig.query.filter_by(company_id=g.user.company_id).first()
if not work_config:
# Create default config for the company
preset = CompanyWorkConfig.get_regional_preset(WorkRegion.GERMANY)
work_config = CompanyWorkConfig(
company_id=g.user.company_id,
standard_hours_per_day=preset['standard_hours_per_day'],
standard_hours_per_week=preset['standard_hours_per_week'],
work_region=WorkRegion.GERMANY,
overtime_enabled=preset['overtime_enabled'],
overtime_rate=preset['overtime_rate'],
double_time_enabled=preset['double_time_enabled'],
double_time_threshold=preset['double_time_threshold'],
double_time_rate=preset['double_time_rate'],
require_breaks=preset['require_breaks'],
break_duration_minutes=preset['break_duration_minutes'],
break_after_hours=preset['break_after_hours'],
weekly_overtime_threshold=preset['weekly_overtime_threshold'],
weekly_overtime_rate=preset['weekly_overtime_rate']
)
db.session.add(work_config)
db.session.flush()
try:
# Handle regional preset selection
if request.form.get('apply_preset'):
region_code = request.form.get('region_preset')
if region_code:
region = WorkRegion(region_code)
preset = CompanyWorkConfig.get_regional_preset(region)
work_config.standard_hours_per_day = preset['standard_hours_per_day']
work_config.standard_hours_per_week = preset['standard_hours_per_week']
work_config.work_region = region
work_config.overtime_enabled = preset['overtime_enabled']
work_config.overtime_rate = preset['overtime_rate']
work_config.double_time_enabled = preset['double_time_enabled']
work_config.double_time_threshold = preset['double_time_threshold']
work_config.double_time_rate = preset['double_time_rate']
work_config.require_breaks = preset['require_breaks']
work_config.break_duration_minutes = preset['break_duration_minutes']
work_config.break_after_hours = preset['break_after_hours']
work_config.weekly_overtime_threshold = preset['weekly_overtime_threshold']
work_config.weekly_overtime_rate = preset['weekly_overtime_rate']
db.session.commit()
flash(f'Applied {preset["region_name"]} work policy preset', 'success')
else:
# Handle manual configuration update
work_config.standard_hours_per_day = float(request.form.get('standard_hours_per_day', 8.0))
work_config.standard_hours_per_week = float(request.form.get('standard_hours_per_week', 40.0))
work_config.overtime_enabled = request.form.get('overtime_enabled') == 'on'
work_config.overtime_rate = float(request.form.get('overtime_rate', 1.5))
work_config.double_time_enabled = request.form.get('double_time_enabled') == 'on'
work_config.double_time_threshold = float(request.form.get('double_time_threshold', 12.0))
work_config.double_time_rate = float(request.form.get('double_time_rate', 2.0))
work_config.require_breaks = request.form.get('require_breaks') == 'on'
work_config.break_duration_minutes = int(request.form.get('break_duration_minutes', 30))
work_config.break_after_hours = float(request.form.get('break_after_hours', 6.0))
work_config.weekly_overtime_threshold = float(request.form.get('weekly_overtime_threshold', 40.0))
work_config.weekly_overtime_rate = float(request.form.get('weekly_overtime_rate', 1.5))
work_config.work_region = WorkRegion.OTHER
# region_name removed - using work_region enum value instead
db.session.commit()
flash('Work policies updated successfully!', 'success')
except ValueError:
flash('Please enter valid numbers for all fields', 'error')
return redirect(url_for('companies.admin_company'))
# Get company statistics
stats = {
'total_users': User.query.filter_by(company_id=company.id).count(),
'total_teams': Team.query.filter_by(company_id=company.id).count(),
'total_projects': Project.query.filter_by(company_id=company.id).count(),
'active_projects': Project.query.filter_by(company_id=company.id, is_active=True).count(),
}
# Get current system settings
settings = {}
for setting in SystemSettings.query.all():
if setting.key == 'registration_enabled':
settings['registration_enabled'] = setting.value == 'true'
elif setting.key == 'email_verification_required':
settings['email_verification_required'] = setting.value == 'true'
# Get or create company work config
work_config = CompanyWorkConfig.query.filter_by(company_id=g.user.company_id).first()
if not work_config:
# Create default config for the company
preset = CompanyWorkConfig.get_regional_preset(WorkRegion.GERMANY)
work_config = CompanyWorkConfig(
company_id=g.user.company_id,
standard_hours_per_day=preset['standard_hours_per_day'],
standard_hours_per_week=preset['standard_hours_per_week'],
work_region=WorkRegion.GERMANY,
overtime_enabled=preset['overtime_enabled'],
overtime_rate=preset['overtime_rate'],
double_time_enabled=preset['double_time_enabled'],
double_time_threshold=preset['double_time_threshold'],
double_time_rate=preset['double_time_rate'],
require_breaks=preset['require_breaks'],
break_duration_minutes=preset['break_duration_minutes'],
break_after_hours=preset['break_after_hours'],
weekly_overtime_threshold=preset['weekly_overtime_threshold'],
weekly_overtime_rate=preset['weekly_overtime_rate']
)
db.session.add(work_config)
db.session.commit()
# Get available regional presets
regional_presets = []
for region in WorkRegion:
preset = CompanyWorkConfig.get_regional_preset(region)
regional_presets.append({
'code': region.value,
'name': preset['region_name'],
'description': f"{preset['standard_hours_per_day']}h/day, {preset['break_duration_minutes']}min break after {preset['break_after_hours']}h"
})
return render_template('admin_company.html',
title='Company Management',
company=company,
stats=stats,
settings=settings,
work_config=work_config,
regional_presets=regional_presets,
WorkRegion=WorkRegion)
@companies_bp.route('/users')
@admin_required
@company_required
def company_users():
"""List all users in the company with detailed information"""
users = User.query.filter_by(company_id=g.company.id).order_by(User.created_at.desc()).all()
# Calculate user statistics
user_stats = {
'total': len(users),
'verified': len([u for u in users if u.is_verified]),
'unverified': len([u for u in users if not u.is_verified]),
'blocked': len([u for u in users if u.is_blocked]),
'active': len([u for u in users if not u.is_blocked and u.is_verified]),
'admins': len([u for u in users if u.role == Role.ADMIN]),
'supervisors': len([u for u in users if u.role == Role.SUPERVISOR]),
'team_leaders': len([u for u in users if u.role == Role.TEAM_LEADER]),
'team_members': len([u for u in users if u.role == Role.TEAM_MEMBER]),
}
return render_template('company_users.html',
title='Company Users',
company=g.company,
users=users,
stats=user_stats)
# Setup company route (separate from company blueprint due to different URL)
def setup_company():
"""Company setup route for creating new companies with admin users"""
existing_companies = Company.query.count()
# Determine access level
is_initial_setup = existing_companies == 0
is_system_admin = g.user and g.user.role == Role.SYSTEM_ADMIN
is_authorized = is_initial_setup or is_system_admin
# Check authorization for non-initial setups
if not is_initial_setup and not is_system_admin:
flash('You do not have permission to create new companies.', 'error')
return redirect(url_for('home') if g.user else url_for('login'))
if request.method == 'POST':
company_name = request.form.get('company_name')
company_description = request.form.get('company_description', '')
admin_username = request.form.get('admin_username')
admin_email = request.form.get('admin_email')
admin_password = request.form.get('admin_password')
confirm_password = request.form.get('confirm_password')
# Validate input
error = None
if not company_name:
error = 'Company name is required'
elif not admin_username:
error = 'Admin username is required'
elif not admin_email:
error = 'Admin email is required'
elif not admin_password:
error = 'Admin password is required'
elif admin_password != confirm_password:
error = 'Passwords do not match'
elif len(admin_password) < 6:
error = 'Password must be at least 6 characters long'
if error is None:
try:
# Generate company slug
slug = re.sub(r'[^\w\s-]', '', company_name.lower())
slug = re.sub(r'[-\s]+', '-', slug).strip('-')
# Ensure slug uniqueness
base_slug = slug
counter = 1
while Company.query.filter_by(slug=slug).first():
slug = f"{base_slug}-{counter}"
counter += 1
# Create company
company = Company(
name=company_name,
slug=slug,
description=company_description,
is_active=True
)
db.session.add(company)
db.session.flush() # Get company.id without committing
# Check if username/email already exists in this company context
existing_user_by_username = User.query.filter_by(
username=admin_username,
company_id=company.id
).first()
existing_user_by_email = User.query.filter_by(
email=admin_email,
company_id=company.id
).first()
if existing_user_by_username:
error = 'Username already exists in this company'
elif existing_user_by_email:
error = 'Email already registered in this company'
if error is None:
# Create admin user
admin_user = User(
username=admin_username,
email=admin_email,
company_id=company.id,
role=Role.ADMIN,
is_verified=True # Auto-verify company admin
)
admin_user.set_password(admin_password)
db.session.add(admin_user)
db.session.commit()
if is_initial_setup:
# Auto-login the admin user for initial setup
session['user_id'] = admin_user.id
session['username'] = admin_user.username
session['role'] = admin_user.role.value
flash(f'Company "{company_name}" created successfully! You are now logged in as the administrator.', 'success')
return redirect(url_for('home'))
else:
# For super admin creating additional companies, don't auto-login
flash(f'Company "{company_name}" created successfully! Admin user "{admin_username}" has been created with the company code "{slug}".', 'success')
return redirect(url_for('companies.admin_company') if g.user else url_for('login'))
else:
db.session.rollback()
except Exception as e:
db.session.rollback()
logger.error(f"Error during company setup: {str(e)}")
error = f"An error occurred during setup: {str(e)}"
if error:
flash(error, 'error')
return render_template('setup_company.html',
title='Company Setup',
existing_companies=existing_companies,
is_initial_setup=is_initial_setup,
is_super_admin=is_system_admin)