Update About page and routing (when not logged in).
This commit is contained in:
159
app.py
159
app.py
@@ -57,10 +57,10 @@ db.init_app(app)
|
|||||||
def run_migrations():
|
def run_migrations():
|
||||||
"""Run all database migrations and initialize system settings."""
|
"""Run all database migrations and initialize system settings."""
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
# Determine database path based on environment
|
# Determine database path based on environment
|
||||||
db_path = '/data/timetrack.db' if os.path.exists('/data') else 'timetrack.db'
|
db_path = '/data/timetrack.db' if os.path.exists('/data') else 'timetrack.db'
|
||||||
|
|
||||||
# Check if database exists
|
# Check if database exists
|
||||||
if not os.path.exists(db_path):
|
if not os.path.exists(db_path):
|
||||||
print("Database doesn't exist. Creating new database.")
|
print("Database doesn't exist. Creating new database.")
|
||||||
@@ -86,7 +86,7 @@ def run_migrations():
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Migrate time_entry table
|
# Migrate time_entry table
|
||||||
cursor.execute("PRAGMA table_info(time_entry)")
|
cursor.execute("PRAGMA table_info(time_entry)")
|
||||||
time_entry_columns = [column[1] for column in cursor.fetchall()]
|
time_entry_columns = [column[1] for column in cursor.fetchall()]
|
||||||
@@ -130,13 +130,13 @@ def run_migrations():
|
|||||||
# Check and add missing columns to work_config
|
# Check and add missing columns to work_config
|
||||||
cursor.execute("PRAGMA table_info(work_config)")
|
cursor.execute("PRAGMA table_info(work_config)")
|
||||||
work_config_columns = [column[1] for column in cursor.fetchall()]
|
work_config_columns = [column[1] for column in cursor.fetchall()]
|
||||||
|
|
||||||
work_config_migrations = [
|
work_config_migrations = [
|
||||||
('additional_break_minutes', "ALTER TABLE work_config ADD COLUMN additional_break_minutes INTEGER DEFAULT 15"),
|
('additional_break_minutes', "ALTER TABLE work_config ADD COLUMN additional_break_minutes INTEGER DEFAULT 15"),
|
||||||
('additional_break_threshold_hours', "ALTER TABLE work_config ADD COLUMN additional_break_threshold_hours FLOAT DEFAULT 9.0"),
|
('additional_break_threshold_hours', "ALTER TABLE work_config ADD COLUMN additional_break_threshold_hours FLOAT DEFAULT 9.0"),
|
||||||
('user_id', "ALTER TABLE work_config ADD COLUMN user_id INTEGER")
|
('user_id', "ALTER TABLE work_config ADD COLUMN user_id INTEGER")
|
||||||
]
|
]
|
||||||
|
|
||||||
for column_name, sql_command in work_config_migrations:
|
for column_name, sql_command in work_config_migrations:
|
||||||
if column_name not in work_config_columns:
|
if column_name not in work_config_columns:
|
||||||
print(f"Adding {column_name} column to work_config...")
|
print(f"Adding {column_name} column to work_config...")
|
||||||
@@ -168,11 +168,11 @@ def run_migrations():
|
|||||||
# First ensure all users have roles set based on is_admin
|
# First ensure all users have roles set based on is_admin
|
||||||
cursor.execute("UPDATE user SET role = 'Administrator' WHERE is_admin = 1 AND (role IS NULL OR role = '')")
|
cursor.execute("UPDATE user SET role = 'Administrator' WHERE is_admin = 1 AND (role IS NULL OR role = '')")
|
||||||
cursor.execute("UPDATE user SET role = 'Team Member' WHERE is_admin = 0 AND (role IS NULL OR role = '')")
|
cursor.execute("UPDATE user SET role = 'Team Member' WHERE is_admin = 0 AND (role IS NULL OR role = '')")
|
||||||
|
|
||||||
# Drop the is_admin column (SQLite requires table recreation)
|
# Drop the is_admin column (SQLite requires table recreation)
|
||||||
print("Removing is_admin column...")
|
print("Removing is_admin column...")
|
||||||
cursor.execute("PRAGMA foreign_keys=off")
|
cursor.execute("PRAGMA foreign_keys=off")
|
||||||
|
|
||||||
# Create new table without is_admin column
|
# Create new table without is_admin column
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
CREATE TABLE user_new (
|
CREATE TABLE user_new (
|
||||||
@@ -191,18 +191,18 @@ def run_migrations():
|
|||||||
two_factor_secret VARCHAR(32)
|
two_factor_secret VARCHAR(32)
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Copy data from old table to new table (excluding is_admin)
|
# Copy data from old table to new table (excluding is_admin)
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT INTO user_new (id, username, email, password_hash, created_at, is_verified,
|
INSERT INTO user_new (id, username, email, password_hash, created_at, is_verified,
|
||||||
verification_token, token_expiry, is_blocked, role, team_id,
|
verification_token, token_expiry, is_blocked, role, team_id,
|
||||||
two_factor_enabled, two_factor_secret)
|
two_factor_enabled, two_factor_secret)
|
||||||
SELECT id, username, email, password_hash, created_at, is_verified,
|
SELECT id, username, email, password_hash, created_at, is_verified,
|
||||||
verification_token, token_expiry, is_blocked, role, team_id,
|
verification_token, token_expiry, is_blocked, role, team_id,
|
||||||
two_factor_enabled, two_factor_secret
|
two_factor_enabled, two_factor_secret
|
||||||
FROM user
|
FROM user
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Drop old table and rename new table
|
# Drop old table and rename new table
|
||||||
cursor.execute("DROP TABLE user")
|
cursor.execute("DROP TABLE user")
|
||||||
cursor.execute("ALTER TABLE user_new RENAME TO user")
|
cursor.execute("ALTER TABLE user_new RENAME TO user")
|
||||||
@@ -278,7 +278,7 @@ def run_migrations():
|
|||||||
|
|
||||||
# Commit all schema changes
|
# Commit all schema changes
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error during database migration: {e}")
|
print(f"Error during database migration: {e}")
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
@@ -288,60 +288,60 @@ def run_migrations():
|
|||||||
|
|
||||||
# Now use SQLAlchemy for data migrations
|
# Now use SQLAlchemy for data migrations
|
||||||
db.create_all() # This will create any remaining tables defined in models
|
db.create_all() # This will create any remaining tables defined in models
|
||||||
|
|
||||||
# Initialize system settings
|
# Initialize system settings
|
||||||
init_system_settings()
|
init_system_settings()
|
||||||
|
|
||||||
# Handle company migration and admin user setup
|
# Handle company migration and admin user setup
|
||||||
migrate_to_company_model()
|
migrate_to_company_model()
|
||||||
migrate_data()
|
migrate_data()
|
||||||
|
|
||||||
print("Database migrations completed successfully!")
|
print("Database migrations completed successfully!")
|
||||||
|
|
||||||
def migrate_to_company_model():
|
def migrate_to_company_model():
|
||||||
"""Migrate existing data to support company model"""
|
"""Migrate existing data to support company model"""
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
# Determine database path based on environment
|
# Determine database path based on environment
|
||||||
db_path = '/data/timetrack.db' if os.path.exists('/data') else 'timetrack.db'
|
db_path = '/data/timetrack.db' if os.path.exists('/data') else 'timetrack.db'
|
||||||
|
|
||||||
# Connect to the database for raw SQL operations
|
# Connect to the database for raw SQL operations
|
||||||
conn = sqlite3.connect(db_path)
|
conn = sqlite3.connect(db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if company_id column exists in user table
|
# Check if company_id column exists in user table
|
||||||
cursor.execute("PRAGMA table_info(user)")
|
cursor.execute("PRAGMA table_info(user)")
|
||||||
user_columns = [column[1] for column in cursor.fetchall()]
|
user_columns = [column[1] for column in cursor.fetchall()]
|
||||||
|
|
||||||
if 'company_id' not in user_columns:
|
if 'company_id' not in user_columns:
|
||||||
print("Migrating to company model...")
|
print("Migrating to company model...")
|
||||||
|
|
||||||
# Add company_id columns to existing tables
|
# Add company_id columns to existing tables
|
||||||
tables_to_migrate = [
|
tables_to_migrate = [
|
||||||
('user', 'ALTER TABLE user ADD COLUMN company_id INTEGER'),
|
('user', 'ALTER TABLE user ADD COLUMN company_id INTEGER'),
|
||||||
('team', 'ALTER TABLE team ADD COLUMN company_id INTEGER'),
|
('team', 'ALTER TABLE team ADD COLUMN company_id INTEGER'),
|
||||||
('project', 'ALTER TABLE project ADD COLUMN company_id INTEGER')
|
('project', 'ALTER TABLE project ADD COLUMN company_id INTEGER')
|
||||||
]
|
]
|
||||||
|
|
||||||
for table_name, sql_command in tables_to_migrate:
|
for table_name, sql_command in tables_to_migrate:
|
||||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||||
columns = [column[1] for column in cursor.fetchall()]
|
columns = [column[1] for column in cursor.fetchall()]
|
||||||
if 'company_id' not in columns:
|
if 'company_id' not in columns:
|
||||||
print(f"Adding company_id column to {table_name}...")
|
print(f"Adding company_id column to {table_name}...")
|
||||||
cursor.execute(sql_command)
|
cursor.execute(sql_command)
|
||||||
|
|
||||||
# Check if there are existing users but no companies
|
# Check if there are existing users but no companies
|
||||||
cursor.execute("SELECT COUNT(*) FROM user")
|
cursor.execute("SELECT COUNT(*) FROM user")
|
||||||
user_count = cursor.fetchone()[0]
|
user_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='company'")
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='company'")
|
||||||
company_table_exists = cursor.fetchone()
|
company_table_exists = cursor.fetchone()
|
||||||
|
|
||||||
if user_count > 0 and company_table_exists:
|
if user_count > 0 and company_table_exists:
|
||||||
cursor.execute("SELECT COUNT(*) FROM company")
|
cursor.execute("SELECT COUNT(*) FROM company")
|
||||||
company_count = cursor.fetchone()[0]
|
company_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
if company_count == 0:
|
if company_count == 0:
|
||||||
print("Creating default company for existing data...")
|
print("Creating default company for existing data...")
|
||||||
# Create default company
|
# Create default company
|
||||||
@@ -349,20 +349,20 @@ def migrate_to_company_model():
|
|||||||
INSERT INTO company (name, slug, description, is_active, max_users)
|
INSERT INTO company (name, slug, description, is_active, max_users)
|
||||||
VALUES ('Default Organization', 'default', 'Migrated from single-tenant installation', 1, 1000)
|
VALUES ('Default Organization', 'default', 'Migrated from single-tenant installation', 1, 1000)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Get the company ID
|
# Get the company ID
|
||||||
cursor.execute("SELECT last_insert_rowid()")
|
cursor.execute("SELECT last_insert_rowid()")
|
||||||
company_id = cursor.fetchone()[0]
|
company_id = cursor.fetchone()[0]
|
||||||
|
|
||||||
# Update all existing records to use the default company
|
# Update all existing records to use the default company
|
||||||
cursor.execute(f"UPDATE user SET company_id = {company_id} WHERE company_id IS NULL")
|
cursor.execute(f"UPDATE user SET company_id = {company_id} WHERE company_id IS NULL")
|
||||||
cursor.execute(f"UPDATE team SET company_id = {company_id} WHERE company_id IS NULL")
|
cursor.execute(f"UPDATE team SET company_id = {company_id} WHERE company_id IS NULL")
|
||||||
cursor.execute(f"UPDATE project SET company_id = {company_id} WHERE company_id IS NULL")
|
cursor.execute(f"UPDATE project SET company_id = {company_id} WHERE company_id IS NULL")
|
||||||
|
|
||||||
print(f"Assigned {user_count} existing users to default company")
|
print(f"Assigned {user_count} existing users to default company")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error during company migration: {e}")
|
print(f"Error during company migration: {e}")
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
@@ -381,7 +381,7 @@ def init_system_settings():
|
|||||||
)
|
)
|
||||||
db.session.add(reg_setting)
|
db.session.add(reg_setting)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
if not SystemSettings.query.filter_by(key='email_verification_required').first():
|
if not SystemSettings.query.filter_by(key='email_verification_required').first():
|
||||||
print("Adding email_verification_required system setting...")
|
print("Adding email_verification_required system setting...")
|
||||||
email_setting = SystemSettings(
|
email_setting = SystemSettings(
|
||||||
@@ -398,7 +398,7 @@ def migrate_data():
|
|||||||
if Company.query.count() == 0:
|
if Company.query.count() == 0:
|
||||||
print("No companies exist, skipping admin user creation. Use company setup instead.")
|
print("No companies exist, skipping admin user creation. Use company setup instead.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if admin user exists in the first company
|
# Check if admin user exists in the first company
|
||||||
default_company = Company.query.first()
|
default_company = Company.query.first()
|
||||||
if default_company:
|
if default_company:
|
||||||
@@ -466,7 +466,7 @@ def migrate_data():
|
|||||||
'description': 'Customer service and technical support activities'
|
'description': 'Customer service and technical support activities'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
for proj_data in sample_projects:
|
for proj_data in sample_projects:
|
||||||
project = Project(
|
project = Project(
|
||||||
name=proj_data['name'],
|
name=proj_data['name'],
|
||||||
@@ -477,7 +477,7 @@ def migrate_data():
|
|||||||
is_active=True
|
is_active=True
|
||||||
)
|
)
|
||||||
db.session.add(project)
|
db.session.add(project)
|
||||||
|
|
||||||
print(f"Created {len(sample_projects)} sample projects for {default_company.name}")
|
print(f"Created {len(sample_projects)} sample projects for {default_company.name}")
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@@ -560,17 +560,17 @@ def company_required(f):
|
|||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if g.user is None:
|
if g.user is None:
|
||||||
return redirect(url_for('login', next=request.url))
|
return redirect(url_for('login', next=request.url))
|
||||||
|
|
||||||
if g.user.company_id is None:
|
if g.user.company_id is None:
|
||||||
flash('You must be associated with a company to access this page.', 'error')
|
flash('You must be associated with a company to access this page.', 'error')
|
||||||
return redirect(url_for('setup_company'))
|
return redirect(url_for('setup_company'))
|
||||||
|
|
||||||
# Set company context
|
# Set company context
|
||||||
g.company = Company.query.get(g.user.company_id)
|
g.company = Company.query.get(g.user.company_id)
|
||||||
if not g.company or not g.company.is_active:
|
if not g.company or not g.company.is_active:
|
||||||
flash('Your company is not active. Please contact support.', 'error')
|
flash('Your company is not active. Please contact support.', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
@@ -588,7 +588,7 @@ def load_logged_in_user():
|
|||||||
g.company = Company.query.get(g.user.company_id)
|
g.company = Company.query.get(g.user.company_id)
|
||||||
else:
|
else:
|
||||||
g.company = None
|
g.company = None
|
||||||
|
|
||||||
# Check if user is verified
|
# Check if user is verified
|
||||||
if not g.user.is_verified and request.endpoint not in ['verify_email', 'static', 'logout', 'setup_company']:
|
if not g.user.is_verified and request.endpoint not in ['verify_email', 'static', 'logout', 'setup_company']:
|
||||||
# Allow unverified users to access only verification and static resources
|
# Allow unverified users to access only verification and static resources
|
||||||
@@ -621,7 +621,7 @@ def home():
|
|||||||
available_projects = []
|
available_projects = []
|
||||||
if g.user.company_id:
|
if g.user.company_id:
|
||||||
all_projects = Project.query.filter_by(
|
all_projects = Project.query.filter_by(
|
||||||
company_id=g.user.company_id,
|
company_id=g.user.company_id,
|
||||||
is_active=True
|
is_active=True
|
||||||
).all()
|
).all()
|
||||||
for project in all_projects:
|
for project in all_projects:
|
||||||
@@ -633,7 +633,7 @@ def home():
|
|||||||
history=history,
|
history=history,
|
||||||
available_projects=available_projects)
|
available_projects=available_projects)
|
||||||
else:
|
else:
|
||||||
return render_template('index.html', title='Home')
|
return render_template('about.html', title='Home')
|
||||||
|
|
||||||
@app.route('/login', methods=['GET', 'POST'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
@@ -729,14 +729,14 @@ def register():
|
|||||||
error = 'Passwords do not match'
|
error = 'Passwords do not match'
|
||||||
elif not company_code:
|
elif not company_code:
|
||||||
error = 'Company code is required'
|
error = 'Company code is required'
|
||||||
|
|
||||||
# Find company by code
|
# Find company by code
|
||||||
company = None
|
company = None
|
||||||
if company_code:
|
if company_code:
|
||||||
company = Company.query.filter_by(slug=company_code.lower()).first()
|
company = Company.query.filter_by(slug=company_code.lower()).first()
|
||||||
if not company:
|
if not company:
|
||||||
error = 'Invalid company code'
|
error = 'Invalid company code'
|
||||||
|
|
||||||
# Check for existing users within the company
|
# Check for existing users within the company
|
||||||
if company and not error:
|
if company and not error:
|
||||||
if User.query.filter_by(username=username, company_id=company.id).first():
|
if User.query.filter_by(username=username, company_id=company.id).first():
|
||||||
@@ -748,13 +748,13 @@ def register():
|
|||||||
try:
|
try:
|
||||||
# Check if this is the first user account in this company
|
# Check if this is the first user account in this company
|
||||||
is_first_user_in_company = User.query.filter_by(company_id=company.id).count() == 0
|
is_first_user_in_company = User.query.filter_by(company_id=company.id).count() == 0
|
||||||
|
|
||||||
# Check if email verification is required
|
# Check if email verification is required
|
||||||
email_verification_required = get_system_setting('email_verification_required', 'true') == 'true'
|
email_verification_required = get_system_setting('email_verification_required', 'true') == 'true'
|
||||||
|
|
||||||
new_user = User(
|
new_user = User(
|
||||||
username=username,
|
username=username,
|
||||||
email=email,
|
email=email,
|
||||||
company_id=company.id,
|
company_id=company.id,
|
||||||
is_verified=False
|
is_verified=False
|
||||||
)
|
)
|
||||||
@@ -818,17 +818,17 @@ The TimeTrack Team
|
|||||||
def setup_company():
|
def setup_company():
|
||||||
"""Company setup route for creating new companies with admin users"""
|
"""Company setup route for creating new companies with admin users"""
|
||||||
existing_companies = Company.query.count()
|
existing_companies = Company.query.count()
|
||||||
|
|
||||||
# Determine access level
|
# Determine access level
|
||||||
is_initial_setup = existing_companies == 0
|
is_initial_setup = existing_companies == 0
|
||||||
is_super_admin = g.user and g.user.role == Role.ADMIN and existing_companies > 0
|
is_super_admin = g.user and g.user.role == Role.ADMIN and existing_companies > 0
|
||||||
is_authorized = is_initial_setup or is_super_admin
|
is_authorized = is_initial_setup or is_super_admin
|
||||||
|
|
||||||
# Check authorization for non-initial setups
|
# Check authorization for non-initial setups
|
||||||
if not is_initial_setup and not is_super_admin:
|
if not is_initial_setup and not is_super_admin:
|
||||||
flash('You do not have permission to create new companies.', 'error')
|
flash('You do not have permission to create new companies.', 'error')
|
||||||
return redirect(url_for('home') if g.user else url_for('login'))
|
return redirect(url_for('home') if g.user else url_for('login'))
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
company_name = request.form.get('company_name')
|
company_name = request.form.get('company_name')
|
||||||
company_description = request.form.get('company_description', '')
|
company_description = request.form.get('company_description', '')
|
||||||
@@ -836,7 +836,7 @@ def setup_company():
|
|||||||
admin_email = request.form.get('admin_email')
|
admin_email = request.form.get('admin_email')
|
||||||
admin_password = request.form.get('admin_password')
|
admin_password = request.form.get('admin_password')
|
||||||
confirm_password = request.form.get('confirm_password')
|
confirm_password = request.form.get('confirm_password')
|
||||||
|
|
||||||
# Validate input
|
# Validate input
|
||||||
error = None
|
error = None
|
||||||
if not company_name:
|
if not company_name:
|
||||||
@@ -851,21 +851,21 @@ def setup_company():
|
|||||||
error = 'Passwords do not match'
|
error = 'Passwords do not match'
|
||||||
elif len(admin_password) < 6:
|
elif len(admin_password) < 6:
|
||||||
error = 'Password must be at least 6 characters long'
|
error = 'Password must be at least 6 characters long'
|
||||||
|
|
||||||
if error is None:
|
if error is None:
|
||||||
try:
|
try:
|
||||||
# Generate company slug
|
# Generate company slug
|
||||||
import re
|
import re
|
||||||
slug = re.sub(r'[^\w\s-]', '', company_name.lower())
|
slug = re.sub(r'[^\w\s-]', '', company_name.lower())
|
||||||
slug = re.sub(r'[-\s]+', '-', slug).strip('-')
|
slug = re.sub(r'[-\s]+', '-', slug).strip('-')
|
||||||
|
|
||||||
# Ensure slug uniqueness
|
# Ensure slug uniqueness
|
||||||
base_slug = slug
|
base_slug = slug
|
||||||
counter = 1
|
counter = 1
|
||||||
while Company.query.filter_by(slug=slug).first():
|
while Company.query.filter_by(slug=slug).first():
|
||||||
slug = f"{base_slug}-{counter}"
|
slug = f"{base_slug}-{counter}"
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
# Create company
|
# Create company
|
||||||
company = Company(
|
company = Company(
|
||||||
name=company_name,
|
name=company_name,
|
||||||
@@ -875,22 +875,22 @@ def setup_company():
|
|||||||
)
|
)
|
||||||
db.session.add(company)
|
db.session.add(company)
|
||||||
db.session.flush() # Get company.id without committing
|
db.session.flush() # Get company.id without committing
|
||||||
|
|
||||||
# Check if username/email already exists in this company context
|
# Check if username/email already exists in this company context
|
||||||
existing_user_by_username = User.query.filter_by(
|
existing_user_by_username = User.query.filter_by(
|
||||||
username=admin_username,
|
username=admin_username,
|
||||||
company_id=company.id
|
company_id=company.id
|
||||||
).first()
|
).first()
|
||||||
existing_user_by_email = User.query.filter_by(
|
existing_user_by_email = User.query.filter_by(
|
||||||
email=admin_email,
|
email=admin_email,
|
||||||
company_id=company.id
|
company_id=company.id
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if existing_user_by_username:
|
if existing_user_by_username:
|
||||||
error = 'Username already exists in this company'
|
error = 'Username already exists in this company'
|
||||||
elif existing_user_by_email:
|
elif existing_user_by_email:
|
||||||
error = 'Email already registered in this company'
|
error = 'Email already registered in this company'
|
||||||
|
|
||||||
if error is None:
|
if error is None:
|
||||||
# Create admin user
|
# Create admin user
|
||||||
admin_user = User(
|
admin_user = User(
|
||||||
@@ -903,13 +903,13 @@ def setup_company():
|
|||||||
admin_user.set_password(admin_password)
|
admin_user.set_password(admin_password)
|
||||||
db.session.add(admin_user)
|
db.session.add(admin_user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
if is_initial_setup:
|
if is_initial_setup:
|
||||||
# Auto-login the admin user for initial setup
|
# Auto-login the admin user for initial setup
|
||||||
session['user_id'] = admin_user.id
|
session['user_id'] = admin_user.id
|
||||||
session['username'] = admin_user.username
|
session['username'] = admin_user.username
|
||||||
session['role'] = admin_user.role.value
|
session['role'] = admin_user.role.value
|
||||||
|
|
||||||
flash(f'Company "{company_name}" created successfully! You are now logged in as the administrator.', 'success')
|
flash(f'Company "{company_name}" created successfully! You are now logged in as the administrator.', 'success')
|
||||||
return redirect(url_for('home'))
|
return redirect(url_for('home'))
|
||||||
else:
|
else:
|
||||||
@@ -918,17 +918,17 @@ def setup_company():
|
|||||||
return redirect(url_for('admin_company') if g.user else url_for('login'))
|
return redirect(url_for('admin_company') if g.user else url_for('login'))
|
||||||
else:
|
else:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
logger.error(f"Error during company setup: {str(e)}")
|
logger.error(f"Error during company setup: {str(e)}")
|
||||||
error = f"An error occurred during setup: {str(e)}"
|
error = f"An error occurred during setup: {str(e)}"
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
flash(error, 'error')
|
flash(error, 'error')
|
||||||
|
|
||||||
return render_template('setup_company.html',
|
return render_template('setup_company.html',
|
||||||
title='Company Setup',
|
title='Company Setup',
|
||||||
existing_companies=existing_companies,
|
existing_companies=existing_companies,
|
||||||
is_initial_setup=is_initial_setup,
|
is_initial_setup=is_initial_setup,
|
||||||
is_super_admin=is_super_admin)
|
is_super_admin=is_super_admin)
|
||||||
@@ -1311,7 +1311,6 @@ def verify_2fa():
|
|||||||
return render_template('verify_2fa.html', title='Two-Factor Authentication')
|
return render_template('verify_2fa.html', title='Two-Factor Authentication')
|
||||||
|
|
||||||
@app.route('/about')
|
@app.route('/about')
|
||||||
@login_required
|
|
||||||
def about():
|
def about():
|
||||||
return render_template('about.html', title='About')
|
return render_template('about.html', title='About')
|
||||||
|
|
||||||
@@ -1625,13 +1624,13 @@ def admin_settings():
|
|||||||
reg_setting = SystemSettings.query.filter_by(key='registration_enabled').first()
|
reg_setting = SystemSettings.query.filter_by(key='registration_enabled').first()
|
||||||
if reg_setting:
|
if reg_setting:
|
||||||
reg_setting.value = 'true' if registration_enabled else 'false'
|
reg_setting.value = 'true' if registration_enabled else 'false'
|
||||||
|
|
||||||
# Update email verification setting
|
# Update email verification setting
|
||||||
email_verification_required = 'email_verification_required' in request.form
|
email_verification_required = 'email_verification_required' in request.form
|
||||||
email_setting = SystemSettings.query.filter_by(key='email_verification_required').first()
|
email_setting = SystemSettings.query.filter_by(key='email_verification_required').first()
|
||||||
if email_setting:
|
if email_setting:
|
||||||
email_setting.value = 'true' if email_verification_required else 'false'
|
email_setting.value = 'true' if email_verification_required else 'false'
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('System settings updated successfully!', 'success')
|
flash('System settings updated successfully!', 'success')
|
||||||
|
|
||||||
@@ -1652,7 +1651,7 @@ def admin_settings():
|
|||||||
def admin_company():
|
def admin_company():
|
||||||
"""View and manage company settings"""
|
"""View and manage company settings"""
|
||||||
company = g.company
|
company = g.company
|
||||||
|
|
||||||
# Get company statistics
|
# Get company statistics
|
||||||
stats = {
|
stats = {
|
||||||
'total_users': User.query.filter_by(company_id=company.id).count(),
|
'total_users': User.query.filter_by(company_id=company.id).count(),
|
||||||
@@ -1660,7 +1659,7 @@ def admin_company():
|
|||||||
'total_projects': Project.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(),
|
'active_projects': Project.query.filter_by(company_id=company.id, is_active=True).count(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return render_template('admin_company.html', title='Company Management', company=company, stats=stats)
|
return render_template('admin_company.html', title='Company Management', company=company, stats=stats)
|
||||||
|
|
||||||
@app.route('/admin/company/edit', methods=['GET', 'POST'])
|
@app.route('/admin/company/edit', methods=['GET', 'POST'])
|
||||||
@@ -1669,20 +1668,20 @@ def admin_company():
|
|||||||
def edit_company():
|
def edit_company():
|
||||||
"""Edit company details"""
|
"""Edit company details"""
|
||||||
company = g.company
|
company = g.company
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
name = request.form.get('name')
|
name = request.form.get('name')
|
||||||
description = request.form.get('description', '')
|
description = request.form.get('description', '')
|
||||||
max_users = request.form.get('max_users')
|
max_users = request.form.get('max_users')
|
||||||
is_active = 'is_active' in request.form
|
is_active = 'is_active' in request.form
|
||||||
|
|
||||||
# Validate input
|
# Validate input
|
||||||
error = None
|
error = None
|
||||||
if not name:
|
if not name:
|
||||||
error = 'Company name is required'
|
error = 'Company name is required'
|
||||||
elif name != company.name and Company.query.filter_by(name=name).first():
|
elif name != company.name and Company.query.filter_by(name=name).first():
|
||||||
error = 'Company name already exists'
|
error = 'Company name already exists'
|
||||||
|
|
||||||
if max_users:
|
if max_users:
|
||||||
try:
|
try:
|
||||||
max_users = int(max_users)
|
max_users = int(max_users)
|
||||||
@@ -1692,19 +1691,19 @@ def edit_company():
|
|||||||
error = 'Maximum users must be a valid number'
|
error = 'Maximum users must be a valid number'
|
||||||
else:
|
else:
|
||||||
max_users = None
|
max_users = None
|
||||||
|
|
||||||
if error is None:
|
if error is None:
|
||||||
company.name = name
|
company.name = name
|
||||||
company.description = description
|
company.description = description
|
||||||
company.max_users = max_users
|
company.max_users = max_users
|
||||||
company.is_active = is_active
|
company.is_active = is_active
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
flash('Company details updated successfully!', 'success')
|
flash('Company details updated successfully!', 'success')
|
||||||
return redirect(url_for('admin_company'))
|
return redirect(url_for('admin_company'))
|
||||||
else:
|
else:
|
||||||
flash(error, 'error')
|
flash(error, 'error')
|
||||||
|
|
||||||
return render_template('edit_company.html', title='Edit Company', company=company)
|
return render_template('edit_company.html', title='Edit Company', company=company)
|
||||||
|
|
||||||
@app.route('/admin/company/users')
|
@app.route('/admin/company/users')
|
||||||
@@ -1713,7 +1712,7 @@ def edit_company():
|
|||||||
def company_users():
|
def company_users():
|
||||||
"""List all users in the company with detailed information"""
|
"""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()
|
users = User.query.filter_by(company_id=g.company.id).order_by(User.created_at.desc()).all()
|
||||||
|
|
||||||
# Calculate user statistics
|
# Calculate user statistics
|
||||||
user_stats = {
|
user_stats = {
|
||||||
'total': len(users),
|
'total': len(users),
|
||||||
@@ -1726,8 +1725,8 @@ def company_users():
|
|||||||
'team_leaders': len([u for u in users if u.role == Role.TEAM_LEADER]),
|
'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]),
|
'team_members': len([u for u in users if u.role == Role.TEAM_MEMBER]),
|
||||||
}
|
}
|
||||||
|
|
||||||
return render_template('company_users.html', title='Company Users',
|
return render_template('company_users.html', title='Company Users',
|
||||||
users=users, stats=user_stats, company=g.company)
|
users=users, stats=user_stats, company=g.company)
|
||||||
|
|
||||||
# Add these routes for team management
|
# Add these routes for team management
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="about-content">
|
<div class="about-content">
|
||||||
<div class="intro">
|
<div class="intro">
|
||||||
<h2>Professional Time Tracking Made Simple</h2>
|
<h2>Professional Time Tracking Made Simple</h2>
|
||||||
<p>TimeTrack is a comprehensive time management solution designed to help organizations and individuals monitor work hours efficiently. Built with simplicity and accuracy in mind, our platform provides the tools you need to track, manage, and analyze time spent on work activities.</p>
|
<p>TimeTrack is a comprehensive multi-tenant time management solution designed to help organizations and individuals monitor work hours efficiently. Built with simplicity and accuracy in mind, our platform provides the tools you need to track, manage, and analyze time spent on work activities across multiple companies and teams.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="features-section">
|
<div class="features-section">
|
||||||
@@ -37,6 +37,11 @@
|
|||||||
<p>Customize work hour requirements, mandatory break durations, and threshold settings to match your organization's policies and labor regulations.</p>
|
<p>Customize work hour requirements, mandatory break durations, and threshold settings to match your organization's policies and labor regulations.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-item">
|
||||||
|
<h3>🏢 Multi-Company Support</h3>
|
||||||
|
<p>Enterprise-ready multi-tenant architecture allows multiple companies to operate independently within a single TimeTrack instance, with complete data isolation and security.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="feature-item">
|
<div class="feature-item">
|
||||||
<h3>🔐 Secure & Reliable</h3>
|
<h3>🔐 Secure & Reliable</h3>
|
||||||
<p>Built with security best practices, user authentication, email verification, and role-based access control to protect your organization's data.</p>
|
<p>Built with security best practices, user authentication, email verification, and role-based access control to protect your organization's data.</p>
|
||||||
@@ -65,7 +70,7 @@
|
|||||||
|
|
||||||
<div class="role-card">
|
<div class="role-card">
|
||||||
<h3>⚙️ Administrator</h3>
|
<h3>⚙️ Administrator</h3>
|
||||||
<p>Full system access including user management, team configuration, system settings, and complete administrative control.</p>
|
<p>Full company access including user management, team configuration, system settings, and complete administrative control within their company. Can also manage company information and setup new user registration.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,7 +91,7 @@
|
|||||||
|
|
||||||
<div class="benefit">
|
<div class="benefit">
|
||||||
<h3>✅ Scalable Solution</h3>
|
<h3>✅ Scalable Solution</h3>
|
||||||
<p>Grows with your organization from small teams to large enterprises. Role-based architecture supports complex organizational structures.</p>
|
<p>Grows with your organization from small teams to large enterprises. Multi-tenant architecture supports multiple companies, complex organizational structures, and unlimited growth potential.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="benefit">
|
<div class="benefit">
|
||||||
@@ -101,6 +106,21 @@
|
|||||||
<p>TimeTrack is built using modern web technologies including Flask (Python), SQLite database, and responsive HTML/CSS/JavaScript frontend. The application features a REST API architecture, secure session management, and email integration for user verification.</p>
|
<p>TimeTrack is built using modern web technologies including Flask (Python), SQLite database, and responsive HTML/CSS/JavaScript frontend. The application features a REST API architecture, secure session management, and email integration for user verification.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="company-setup-section">
|
||||||
|
<h2>Company Setup & Registration</h2>
|
||||||
|
<div class="setup-options">
|
||||||
|
<div class="setup-option">
|
||||||
|
<h3>🏢 New Company Setup</h3>
|
||||||
|
<p>Setting up TimeTrack for the first time or adding a new company? Create your company profile and administrator account to get started.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setup-option">
|
||||||
|
<h3>👥 Join Existing Company</h3>
|
||||||
|
<p>Already have a company using TimeTrack? Get your company code from your administrator and register to join your organization.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="support-section">
|
<div class="support-section">
|
||||||
<h2>Getting Started</h2>
|
<h2>Getting Started</h2>
|
||||||
<p>Ready to start tracking your time more effectively? <a href="{{ url_for('register') }}">Create an account</a> to begin, or <a href="{{ url_for('login') }}">log in</a> if you already have access. For questions or support, contact your system administrator.</p>
|
<p>Ready to start tracking your time more effectively? <a href="{{ url_for('register') }}">Create an account</a> to begin, or <a href="{{ url_for('login') }}">log in</a> if you already have access. For questions or support, contact your system administrator.</p>
|
||||||
@@ -216,14 +236,49 @@
|
|||||||
color: #cceeff;
|
color: #cceeff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.company-setup-section {
|
||||||
|
margin: 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-option {
|
||||||
|
background: #f0f8ff;
|
||||||
|
border: 1px solid #b3d9ff;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-option h3 {
|
||||||
|
color: #0066cc;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-option p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.feature-grid, .role-cards, .benefits-list {
|
.feature-grid, .role-cards, .benefits-list, .setup-options {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefits-list .benefit {
|
.benefits-list .benefit {
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setup-options .setup-option {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user