Merge website-branding feature and adjust for compatibility
- Resolved conflicts in models.py, app.py, and template files - Added branding checks to prevent errors when g.branding is None - Updated all template references to use conditional branding - Added BrandingSettings to migrations - Created branding uploads directory - Integrated branding with existing comment and task management features
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -146,3 +146,7 @@ node_modules/
|
|||||||
# Temporary files
|
# Temporary files
|
||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
|
|
||||||
|
# User uploaded content
|
||||||
|
static/uploads/avatars/*
|
||||||
|
!static/uploads/avatars/.gitkeep
|
||||||
77
app.py
77
app.py
@@ -1,5 +1,5 @@
|
|||||||
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file
|
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file
|
||||||
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility
|
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType, ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, TaskDependency, Sprint, SprintStatus, Announcement, SystemEvent, WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility, BrandingSettings
|
||||||
from data_formatting import (
|
from data_formatting import (
|
||||||
format_duration, prepare_export_data, prepare_team_hours_export_data,
|
format_duration, prepare_export_data, prepare_team_hours_export_data,
|
||||||
format_table_data, format_graph_data, format_team_data, format_burndown_data
|
format_table_data, format_graph_data, format_team_data, format_burndown_data
|
||||||
@@ -41,7 +41,7 @@ app.config['MAIL_PORT'] = int(os.environ.get('MAIL_PORT') or 587)
|
|||||||
app.config['MAIL_USE_TLS'] = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
|
app.config['MAIL_USE_TLS'] = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
|
||||||
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME', 'your-email@example.com')
|
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME', 'your-email@example.com')
|
||||||
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD', 'your-password')
|
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD', 'your-password')
|
||||||
app.config['MAIL_DEFAULT_SENDER'] = os.environ.get('MAIL_DEFAULT_SENDER', 'TimeTrack <noreply@timetrack.com>')
|
app.config['MAIL_DEFAULT_SENDER'] = os.environ.get('MAIL_DEFAULT_SENDER', 'TimeTrack <noreply@timetrack.com>') # Will be overridden by branding in mail sending functions
|
||||||
|
|
||||||
# Log mail configuration (without password)
|
# Log mail configuration (without password)
|
||||||
logger.info(f"Mail server: {app.config['MAIL_SERVER']}")
|
logger.info(f"Mail server: {app.config['MAIL_SERVER']}")
|
||||||
@@ -407,6 +407,9 @@ def load_logged_in_user():
|
|||||||
else:
|
else:
|
||||||
g.company = None
|
g.company = None
|
||||||
|
|
||||||
|
# Load branding settings
|
||||||
|
g.branding = BrandingSettings.get_current()
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def home():
|
def home():
|
||||||
if g.user:
|
if g.user:
|
||||||
@@ -638,19 +641,19 @@ def register():
|
|||||||
else:
|
else:
|
||||||
# Send verification email for regular users when verification is required
|
# Send verification email for regular users when verification is required
|
||||||
verification_url = url_for('verify_email', token=token, _external=True)
|
verification_url = url_for('verify_email', token=token, _external=True)
|
||||||
msg = Message('Verify your TimeTrack account', recipients=[email])
|
msg = Message(f'Verify your {g.branding.app_name} account', recipients=[email])
|
||||||
msg.body = f'''Hello {username},
|
msg.body = f'''Hello {username},
|
||||||
|
|
||||||
Thank you for registering with TimeTrack. To complete your registration, please click on the link below:
|
Thank you for registering with {g.branding.app_name}. To complete your registration, please click on the link below:
|
||||||
|
|
||||||
{verification_url}
|
{verification_url}
|
||||||
|
|
||||||
This link will expire in 24 hours.
|
This link will expire in 24 hours.
|
||||||
|
|
||||||
If you did not register for TimeTrack, please ignore this email.
|
If you did not register for {g.branding.app_name}, please ignore this email.
|
||||||
|
|
||||||
Best regards,
|
Best regards,
|
||||||
The TimeTrack Team
|
The {g.branding.app_name} Team
|
||||||
'''
|
'''
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
logger.info(f"Verification email sent to {email}")
|
logger.info(f"Verification email sent to {email}")
|
||||||
@@ -967,17 +970,17 @@ def create_user():
|
|||||||
# Generate verification token and send email
|
# Generate verification token and send email
|
||||||
token = new_user.generate_verification_token()
|
token = new_user.generate_verification_token()
|
||||||
verification_url = url_for('verify_email', token=token, _external=True)
|
verification_url = url_for('verify_email', token=token, _external=True)
|
||||||
msg = Message('Verify your TimeTrack account', recipients=[email])
|
msg = Message(f'Verify your {g.branding.app_name} account', recipients=[email])
|
||||||
msg.body = f'''Hello {username},
|
msg.body = f'''Hello {username},
|
||||||
|
|
||||||
An administrator has created an account for you on TimeTrack. To activate your account, please click on the link below:
|
An administrator has created an account for you on {g.branding.app_name}. To activate your account, please click on the link below:
|
||||||
|
|
||||||
{verification_url}
|
{verification_url}
|
||||||
|
|
||||||
This link will expire in 24 hours.
|
This link will expire in 24 hours.
|
||||||
|
|
||||||
Best regards,
|
Best regards,
|
||||||
The TimeTrack Team
|
The {g.branding.app_name} Team
|
||||||
'''
|
'''
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
|
||||||
@@ -1490,7 +1493,7 @@ def setup_2fa():
|
|||||||
import io
|
import io
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
qr_uri = g.user.get_2fa_uri()
|
qr_uri = g.user.get_2fa_uri(issuer_name=g.branding.app_name)
|
||||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||||
qr.add_data(qr_uri)
|
qr.add_data(qr_uri)
|
||||||
qr.make(fit=True)
|
qr.make(fit=True)
|
||||||
@@ -2469,6 +2472,60 @@ def system_admin_settings():
|
|||||||
total_users=total_users,
|
total_users=total_users,
|
||||||
total_system_admins=total_system_admins)
|
total_system_admins=total_system_admins)
|
||||||
|
|
||||||
|
@app.route('/system-admin/branding', methods=['GET', 'POST'])
|
||||||
|
@system_admin_required
|
||||||
|
def system_admin_branding():
|
||||||
|
"""System Admin: Branding settings"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
branding = BrandingSettings.get_current()
|
||||||
|
|
||||||
|
# Handle form data
|
||||||
|
branding.app_name = request.form.get('app_name', g.branding.app_name).strip()
|
||||||
|
branding.logo_alt_text = request.form.get('logo_alt_text', '').strip()
|
||||||
|
branding.primary_color = request.form.get('primary_color', '#007bff').strip()
|
||||||
|
branding.updated_by_id = g.user.id
|
||||||
|
|
||||||
|
# Handle logo upload
|
||||||
|
if 'logo_file' in request.files:
|
||||||
|
logo_file = request.files['logo_file']
|
||||||
|
if logo_file and logo_file.filename:
|
||||||
|
# Create uploads directory if it doesn't exist
|
||||||
|
upload_dir = os.path.join(app.static_folder, 'uploads', 'branding')
|
||||||
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Save the file with a timestamp to avoid conflicts
|
||||||
|
import time
|
||||||
|
filename = f"logo_{int(time.time())}_{logo_file.filename}"
|
||||||
|
logo_path = os.path.join(upload_dir, filename)
|
||||||
|
logo_file.save(logo_path)
|
||||||
|
branding.logo_filename = filename
|
||||||
|
|
||||||
|
# Handle favicon upload
|
||||||
|
if 'favicon_file' in request.files:
|
||||||
|
favicon_file = request.files['favicon_file']
|
||||||
|
if favicon_file and favicon_file.filename:
|
||||||
|
# Create uploads directory if it doesn't exist
|
||||||
|
upload_dir = os.path.join(app.static_folder, 'uploads', 'branding')
|
||||||
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Save the file with a timestamp to avoid conflicts
|
||||||
|
import time
|
||||||
|
filename = f"favicon_{int(time.time())}_{favicon_file.filename}"
|
||||||
|
favicon_path = os.path.join(upload_dir, filename)
|
||||||
|
favicon_file.save(favicon_path)
|
||||||
|
branding.favicon_filename = filename
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash('Branding settings updated successfully.', 'success')
|
||||||
|
return redirect(url_for('system_admin_branding'))
|
||||||
|
|
||||||
|
# Get current branding settings
|
||||||
|
branding = BrandingSettings.get_current()
|
||||||
|
|
||||||
|
return render_template('system_admin_branding.html',
|
||||||
|
title='System Administrator - Branding Settings',
|
||||||
|
branding=branding)
|
||||||
|
|
||||||
@app.route('/system-admin/health')
|
@app.route('/system-admin/health')
|
||||||
@system_admin_required
|
@system_admin_required
|
||||||
def system_admin_health():
|
def system_admin_health():
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ try:
|
|||||||
from models import (User, TimeEntry, WorkConfig, SystemSettings, Team, Role, Project,
|
from models import (User, TimeEntry, WorkConfig, SystemSettings, Team, Role, Project,
|
||||||
Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType,
|
Company, CompanyWorkConfig, CompanySettings, UserPreferences, WorkRegion, AccountType,
|
||||||
ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement, SystemEvent,
|
ProjectCategory, Task, SubTask, TaskStatus, TaskPriority, Announcement, SystemEvent,
|
||||||
WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility)
|
WidgetType, UserDashboard, DashboardWidget, WidgetTemplate, Comment, CommentVisibility,
|
||||||
|
BrandingSettings)
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
FLASK_AVAILABLE = True
|
FLASK_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -327,6 +328,25 @@ def create_missing_tables(cursor):
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Branding Settings table
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='branding_settings'")
|
||||||
|
if not cursor.fetchone():
|
||||||
|
print("Creating branding_settings table...")
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE branding_settings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
app_name VARCHAR(100) NOT NULL DEFAULT 'Time Tracker',
|
||||||
|
logo_filename VARCHAR(255),
|
||||||
|
logo_alt_text VARCHAR(255) DEFAULT 'Logo',
|
||||||
|
favicon_filename VARCHAR(255),
|
||||||
|
primary_color VARCHAR(7) DEFAULT '#007bff',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_by_id INTEGER,
|
||||||
|
FOREIGN KEY (updated_by_id) REFERENCES user (id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
def migrate_to_company_model(db_path):
|
def migrate_to_company_model(db_path):
|
||||||
"""Migrate to company-based multi-tenancy model."""
|
"""Migrate to company-based multi-tenancy model."""
|
||||||
@@ -1199,6 +1219,31 @@ def migrate_postgresql_schema():
|
|||||||
db.session.execute(text("CREATE INDEX idx_comment_created_at ON comment(created_at DESC)"))
|
db.session.execute(text("CREATE INDEX idx_comment_created_at ON comment(created_at DESC)"))
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
# Check if branding_settings table exists
|
||||||
|
result = db.session.execute(text("""
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_name = 'branding_settings'
|
||||||
|
"""))
|
||||||
|
|
||||||
|
if not result.fetchone():
|
||||||
|
print("Creating branding_settings table...")
|
||||||
|
db.session.execute(text("""
|
||||||
|
CREATE TABLE branding_settings (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
app_name VARCHAR(100) NOT NULL DEFAULT 'Time Tracker',
|
||||||
|
logo_filename VARCHAR(255),
|
||||||
|
logo_alt_text VARCHAR(255) DEFAULT 'Logo',
|
||||||
|
favicon_filename VARCHAR(255),
|
||||||
|
primary_color VARCHAR(7) DEFAULT '#007bff',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_by_id INTEGER,
|
||||||
|
FOREIGN KEY (updated_by_id) REFERENCES "user" (id)
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
# Check if company_settings table exists
|
# Check if company_settings table exists
|
||||||
result = db.session.execute(text("""
|
result = db.session.execute(text("""
|
||||||
SELECT table_name
|
SELECT table_name
|
||||||
|
|||||||
38
models.py
38
models.py
@@ -196,15 +196,17 @@ class User(db.Model):
|
|||||||
self.two_factor_secret = pyotp.random_base32()
|
self.two_factor_secret = pyotp.random_base32()
|
||||||
return self.two_factor_secret
|
return self.two_factor_secret
|
||||||
|
|
||||||
def get_2fa_uri(self):
|
def get_2fa_uri(self, issuer_name=None):
|
||||||
"""Get the provisioning URI for QR code generation"""
|
"""Get the provisioning URI for QR code generation"""
|
||||||
if not self.two_factor_secret:
|
if not self.two_factor_secret:
|
||||||
return None
|
return None
|
||||||
import pyotp
|
import pyotp
|
||||||
totp = pyotp.TOTP(self.two_factor_secret)
|
totp = pyotp.TOTP(self.two_factor_secret)
|
||||||
|
if issuer_name is None:
|
||||||
|
issuer_name = "Time Tracker" # Default fallback
|
||||||
return totp.provisioning_uri(
|
return totp.provisioning_uri(
|
||||||
name=self.email,
|
name=self.email,
|
||||||
issuer_name="TimeTrack"
|
issuer_name=issuer_name
|
||||||
)
|
)
|
||||||
|
|
||||||
def verify_2fa_token(self, token, allow_setup=False):
|
def verify_2fa_token(self, token, allow_setup=False):
|
||||||
@@ -267,6 +269,38 @@ class SystemSettings(db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'<SystemSettings {self.key}={self.value}>'
|
return f'<SystemSettings {self.key}={self.value}>'
|
||||||
|
|
||||||
|
class BrandingSettings(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
app_name = db.Column(db.String(100), nullable=False, default='Time Tracker')
|
||||||
|
logo_filename = db.Column(db.String(255), nullable=True) # Filename of uploaded logo
|
||||||
|
logo_alt_text = db.Column(db.String(255), nullable=True, default='Logo')
|
||||||
|
favicon_filename = db.Column(db.String(255), nullable=True) # Filename of uploaded favicon
|
||||||
|
primary_color = db.Column(db.String(7), nullable=True, default='#007bff') # Hex color
|
||||||
|
|
||||||
|
# Meta fields
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
updated_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
updated_by = db.relationship('User', foreign_keys=[updated_by_id])
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'<BrandingSettings {self.app_name}>'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_current():
|
||||||
|
"""Get current branding settings or create defaults"""
|
||||||
|
settings = BrandingSettings.query.first()
|
||||||
|
if not settings:
|
||||||
|
settings = BrandingSettings(
|
||||||
|
app_name='Time Tracker',
|
||||||
|
logo_alt_text='Application Logo'
|
||||||
|
)
|
||||||
|
db.session.add(settings)
|
||||||
|
db.session.commit()
|
||||||
|
return settings
|
||||||
|
|
||||||
class TimeEntry(db.Model):
|
class TimeEntry(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
arrival_time = db.Column(db.DateTime, nullable=False)
|
arrival_time = db.Column(db.DateTime, nullable=False)
|
||||||
|
|||||||
@@ -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 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>
|
<p>{{ g.branding.app_name }} 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">
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<div class="feature-item">
|
<div class="feature-item">
|
||||||
<h3>🏢 Multi-Company Support</h3>
|
<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>
|
<p>Enterprise-ready multi-tenant architecture allows multiple companies to operate independently within a single {{ g.branding.app_name }} instance, with complete data isolation and security.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="feature-item">
|
<div class="feature-item">
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="benefits-section">
|
<div class="benefits-section">
|
||||||
<h2>Why Choose TimeTrack?</h2>
|
<h2>Why Choose {{ g.branding.app_name }}?</h2>
|
||||||
|
|
||||||
<div class="benefits-list">
|
<div class="benefits-list">
|
||||||
<div class="benefit">
|
<div class="benefit">
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
|
|
||||||
<div class="technical-section">
|
<div class="technical-section">
|
||||||
<h2>Technical Information</h2>
|
<h2>Technical Information</h2>
|
||||||
<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>{{ g.branding.app_name }} 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">
|
<div class="company-setup-section">
|
||||||
@@ -111,12 +111,12 @@
|
|||||||
<div class="setup-options">
|
<div class="setup-options">
|
||||||
<div class="setup-option">
|
<div class="setup-option">
|
||||||
<h3>🏢 New Company Setup</h3>
|
<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>
|
<p>Setting up {{ g.branding.app_name }} for the first time or adding a new company? Create your company profile and administrator account to get started.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setup-option">
|
<div class="setup-option">
|
||||||
<h3>👥 Join Existing Company</h3>
|
<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>
|
<p>Already have a company using {{ g.branding.app_name }}? Get your company code from your administrator and register to join your organization.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<section class="splash-hero">
|
<section class="splash-hero">
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<h1 class="hero-title">Transform Your Productivity</h1>
|
<h1 class="hero-title">Transform Your Productivity</h1>
|
||||||
<p class="hero-subtitle">Experience the future of time management with TimeTrack's intelligent tracking system</p>
|
<p class="hero-subtitle">Experience the future of time management with {{ g.branding.app_name if g.branding else 'TimeTrack' }}'s intelligent tracking system</p>
|
||||||
<div class="cta-buttons">
|
<div class="cta-buttons">
|
||||||
<a href="{{ url_for('register') }}" class="btn-primary">Get Started Free</a>
|
<a href="{{ url_for('register') }}" class="btn-primary">Get Started Free</a>
|
||||||
<a href="{{ url_for('login') }}" class="btn-secondary">Sign In</a>
|
<a href="{{ url_for('login') }}" class="btn-secondary">Sign In</a>
|
||||||
|
|||||||
@@ -3,19 +3,69 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{ title }} - TimeTrack{% if g.company %} - {{ g.company.name }}{% endif %}</title>
|
<title>{{ title }} - {{ g.branding.app_name if g.branding else 'TimeTrack' }}{% if g.company %} - {{ g.company.name }}{% endif %}</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
{% if not g.user %}
|
{% if not g.user %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/splash.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/splash.css') }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if g.branding and g.branding.favicon_filename %}
|
||||||
|
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='uploads/branding/' + g.branding.favicon_filename) }}">
|
||||||
|
{% endif %}
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: {{ g.branding.primary_color if g.branding else '#007bff' }};
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: {{ (g.branding.primary_color if g.branding else '#007bff') + 'dd' }};
|
||||||
|
border-color: {{ (g.branding.primary_color if g.branding else '#007bff') + 'dd' }};
|
||||||
|
}
|
||||||
|
.nav-icon {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.mobile-logo {
|
||||||
|
max-height: 30px;
|
||||||
|
max-width: 150px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.sidebar-logo {
|
||||||
|
max-height: 32px;
|
||||||
|
max-width: 160px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.mobile-nav-brand a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.sidebar-header h2 a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body{% if g.user %} class="has-user"{% endif %}>
|
<body{% if g.user %} class="has-user"{% endif %}>
|
||||||
<!-- Mobile header -->
|
<!-- Mobile header -->
|
||||||
{% if g.user %}
|
{% if g.user %}
|
||||||
<header class="mobile-header">
|
<header class="mobile-header">
|
||||||
<div class="mobile-nav-brand">
|
<div class="mobile-nav-brand">
|
||||||
<a href="{{ url_for('home') }}">TimeTrack</a>
|
<a href="{{ url_for('home') }}">
|
||||||
|
{% if g.branding and g.branding.logo_filename %}
|
||||||
|
<img src="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename) }}"
|
||||||
|
alt="{{ g.branding.logo_alt_text }}"
|
||||||
|
class="mobile-logo">
|
||||||
|
{% else %}
|
||||||
|
{{ g.branding.app_name if g.branding else 'TimeTrack' }}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
{% if g.company %}
|
{% if g.company %}
|
||||||
<small class="company-name">{{ g.company.name }}</small>
|
<small class="company-name">{{ g.company.name }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -32,6 +82,22 @@
|
|||||||
{% if g.user %}
|
{% if g.user %}
|
||||||
<aside class="sidebar" id="sidebar">
|
<aside class="sidebar" id="sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
|
<h2>
|
||||||
|
<a href="{{ url_for('home') }}">
|
||||||
|
{% if g.branding and g.branding.logo_filename %}
|
||||||
|
<img src="{{ url_for('static', filename='uploads/branding/' + g.branding.logo_filename) }}"
|
||||||
|
alt="{{ g.branding.logo_alt_text }}"
|
||||||
|
class="sidebar-logo">
|
||||||
|
{% else %}
|
||||||
|
{{ g.branding.app_name if g.branding else 'TimeTrack' }}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
{% if g.company %}
|
||||||
|
<div class="company-info">
|
||||||
|
<small class="text-muted">{{ g.company.name }}</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<button class="sidebar-toggle" id="sidebar-toggle">
|
<button class="sidebar-toggle" id="sidebar-toggle">
|
||||||
<span></span>
|
<span></span>
|
||||||
<span></span>
|
<span></span>
|
||||||
@@ -151,7 +217,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© {{ current_year }} TimeTrack. All rights reserved.</p>
|
<p>© {{ current_year }} {{ g.branding.app_name }}. All rights reserved.</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/script.js') }}"></script>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Login - TimeTrack</title>
|
<title>Login - {{ g.branding.app_name if g.branding else 'TimeTrack' }}</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/auth.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/auth.css') }}">
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="auth-container">
|
<div class="auth-container">
|
||||||
<div class="auth-brand">
|
<div class="auth-brand">
|
||||||
<h1>Welcome Back</h1>
|
<h1>Welcome Back</h1>
|
||||||
<p>Sign in to continue tracking</p>
|
<p>Sign in to {{ g.branding.app_name if g.branding else 'TimeTrack' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Register - TimeTrack</title>
|
<title>Register - {{ g.branding.app_name if g.branding else 'TimeTrack' }}</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/auth.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/auth.css') }}">
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<body class="auth-page">
|
<body class="auth-page">
|
||||||
<div class="auth-container">
|
<div class="auth-container">
|
||||||
<div class="auth-brand">
|
<div class="auth-brand">
|
||||||
<h1>Welcome to TimeTrack</h1>
|
<h1>Welcome to {{ g.branding.app_name if g.branding else 'TimeTrack' }}</h1>
|
||||||
<p>Join your company team</p>
|
<p>Join your company team</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="manual-entry">
|
<div class="manual-entry">
|
||||||
<h3>Can't scan? Enter this code manually:</h3>
|
<h3>Can't scan? Enter this code manually:</h3>
|
||||||
<div class="secret-code">{{ secret }}</div>
|
<div class="secret-code">{{ secret }}</div>
|
||||||
<p><small>Account: {{ g.user.email }}<br>Issuer: TimeTrack</small></p>
|
<p><small>Account: {{ g.user.email }}<br>Issuer: {{ g.branding.app_name }}</small></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="admin-container">
|
<div class="admin-container">
|
||||||
<h1>
|
<h1>
|
||||||
{% if is_initial_setup %}
|
{% if is_initial_setup %}
|
||||||
Welcome to TimeTrack
|
Welcome to {{ g.branding.app_name }}
|
||||||
{% else %}
|
{% else %}
|
||||||
Create New Company
|
Create New Company
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -14,12 +14,12 @@
|
|||||||
{% if is_initial_setup %}
|
{% if is_initial_setup %}
|
||||||
<div class="info-message">
|
<div class="info-message">
|
||||||
<h3>🎉 Let's Get Started!</h3>
|
<h3>🎉 Let's Get Started!</h3>
|
||||||
<p>Set up your company and create the first administrator account to begin using TimeTrack.</p>
|
<p>Set up your company and create the first administrator account to begin using {{ g.branding.app_name }}.</p>
|
||||||
</div>
|
</div>
|
||||||
{% elif is_super_admin %}
|
{% elif is_super_admin %}
|
||||||
<div class="info-message">
|
<div class="info-message">
|
||||||
<h3>🏢 New Company Setup</h3>
|
<h3>🏢 New Company Setup</h3>
|
||||||
<p>Create a new company with its own administrator. This will be a separate organization within TimeTrack.</p>
|
<p>Create a new company with its own administrator. This will be a separate organization within {{ g.branding.app_name }}.</p>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="error-message">
|
<div class="error-message">
|
||||||
|
|||||||
222
templates/system_admin_branding.html
Normal file
222
templates/system_admin_branding.html
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="header-section">
|
||||||
|
<h1>🎨 System Administrator - Branding Settings</h1>
|
||||||
|
<p class="subtitle">Customize the appearance and branding of your TimeTrack instance</p>
|
||||||
|
<a href="{{ url_for('system_admin_dashboard') }}" class="btn btn-secondary">← Back to Dashboard</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Branding Preview -->
|
||||||
|
<div class="stats-section">
|
||||||
|
<h3>👁️ Current Branding Preview</h3>
|
||||||
|
<div class="branding-preview">
|
||||||
|
<div class="preview-card">
|
||||||
|
<div class="preview-header">
|
||||||
|
{% if branding.logo_filename %}
|
||||||
|
<img src="{{ url_for('static', filename='uploads/branding/' + branding.logo_filename) }}"
|
||||||
|
alt="{{ branding.logo_alt_text }}"
|
||||||
|
class="preview-logo">
|
||||||
|
{% endif %}
|
||||||
|
<h4>{{ branding.app_name }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="preview-content">
|
||||||
|
<p>This is how your branding will appear to users.</p>
|
||||||
|
<div class="preview-button" style="background-color: {{ branding.primary_color }};">
|
||||||
|
Sample Button
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Branding Settings Form -->
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3>🔧 Branding Configuration</h3>
|
||||||
|
<form method="POST" enctype="multipart/form-data" class="settings-form">
|
||||||
|
<!-- Application Name -->
|
||||||
|
<div class="setting-group">
|
||||||
|
<div class="setting-header">
|
||||||
|
<h4>Application Name</h4>
|
||||||
|
<p>The name that appears throughout the application</p>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<label for="app_name">Application Name:</label>
|
||||||
|
<input type="text" id="app_name" name="app_name"
|
||||||
|
value="{{ branding.app_name }}"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="TimeTrack"
|
||||||
|
required>
|
||||||
|
<small class="setting-description">
|
||||||
|
This name will appear in the title, navigation, and throughout the interface.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logo Upload -->
|
||||||
|
<div class="setting-group">
|
||||||
|
<div class="setting-header">
|
||||||
|
<h4>Logo Upload</h4>
|
||||||
|
<p>Upload a custom logo for your application</p>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<label for="logo_file">Logo File:</label>
|
||||||
|
<input type="file" id="logo_file" name="logo_file"
|
||||||
|
accept="image/*"
|
||||||
|
class="form-control">
|
||||||
|
{% if branding.logo_filename %}
|
||||||
|
<div class="current-file">
|
||||||
|
<strong>Current logo:</strong>
|
||||||
|
<img src="{{ url_for('static', filename='uploads/branding/' + branding.logo_filename) }}"
|
||||||
|
alt="{{ branding.logo_alt_text }}"
|
||||||
|
class="current-logo">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<small class="setting-description">
|
||||||
|
Supported formats: PNG, JPG, GIF, SVG. Recommended size: 200x50px or similar aspect ratio.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logo Alt Text -->
|
||||||
|
<div class="setting-group">
|
||||||
|
<div class="setting-header">
|
||||||
|
<h4>Logo Alt Text</h4>
|
||||||
|
<p>Alternative text for the logo (accessibility)</p>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<label for="logo_alt_text">Alt Text:</label>
|
||||||
|
<input type="text" id="logo_alt_text" name="logo_alt_text"
|
||||||
|
value="{{ branding.logo_alt_text }}"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Company Logo">
|
||||||
|
<small class="setting-description">
|
||||||
|
This text will be read by screen readers and shown when the logo cannot be displayed.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Favicon Upload -->
|
||||||
|
<div class="setting-group">
|
||||||
|
<div class="setting-header">
|
||||||
|
<h4>Favicon Upload</h4>
|
||||||
|
<p>Upload a custom favicon (browser tab icon)</p>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<label for="favicon_file">Favicon File:</label>
|
||||||
|
<input type="file" id="favicon_file" name="favicon_file"
|
||||||
|
accept="image/*"
|
||||||
|
class="form-control">
|
||||||
|
{% if branding.favicon_filename %}
|
||||||
|
<div class="current-file">
|
||||||
|
<strong>Current favicon:</strong>
|
||||||
|
<img src="{{ url_for('static', filename='uploads/branding/' + branding.favicon_filename) }}"
|
||||||
|
alt="Current favicon"
|
||||||
|
class="current-favicon">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<small class="setting-description">
|
||||||
|
Supported formats: ICO, PNG (16x16px or 32x32px recommended).
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Primary Color -->
|
||||||
|
<div class="setting-group">
|
||||||
|
<div class="setting-header">
|
||||||
|
<h4>Primary Color</h4>
|
||||||
|
<p>The primary color used throughout the application</p>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control">
|
||||||
|
<label for="primary_color">Primary Color:</label>
|
||||||
|
<input type="color" id="primary_color" name="primary_color"
|
||||||
|
value="{{ branding.primary_color }}"
|
||||||
|
class="form-control color-picker">
|
||||||
|
<small class="setting-description">
|
||||||
|
This color will be used for buttons, links, and other UI elements throughout the application.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Save Button -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">💾 Save Branding Settings</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Branding-specific styles that complement the existing design system */
|
||||||
|
.branding-preview {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-logo {
|
||||||
|
max-height: 40px;
|
||||||
|
max-width: 150px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-button {
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-file {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-logo {
|
||||||
|
max-height: 30px;
|
||||||
|
max-width: 100px;
|
||||||
|
object-fit: contain;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-favicon {
|
||||||
|
max-height: 16px;
|
||||||
|
max-width: 16px;
|
||||||
|
object-fit: contain;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker {
|
||||||
|
width: 80px !important;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
@@ -183,7 +183,10 @@
|
|||||||
<a href="{{ url_for('system_admin_settings') }}" class="btn btn-primary">
|
<a href="{{ url_for('system_admin_settings') }}" class="btn btn-primary">
|
||||||
⚙️ System Settings
|
⚙️ System Settings
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('system_admin_health') }}" class="btn btn-warning">
|
<a href="{{ url_for('system_admin_branding') }}" class="btn btn-primary">
|
||||||
|
🎨 Branding Settings
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('system_admin_health') }}" class="btn btn-primary">
|
||||||
🏥 System Health
|
🏥 System Health
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
<div class="info-grid">
|
<div class="info-grid">
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<h4>Application Version</h4>
|
<h4>Application Version</h4>
|
||||||
<p>TimeTrack v1.0</p>
|
<p>{{ g.branding.app_name }} v1.0</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-card">
|
<div class="info-card">
|
||||||
<h4>Database</h4>
|
<h4>Database</h4>
|
||||||
|
|||||||
Reference in New Issue
Block a user