Add company feature.

This commit is contained in:
2025-07-02 12:42:18 +02:00
committed by Jens Luedicke
parent 85847b5d39
commit 8f49958dfa
17 changed files with 1465 additions and 216 deletions

781
app.py

File diff suppressed because it is too large Load Diff

View File

@@ -205,7 +205,6 @@ def migrate_database():
admin = User(
username='admin',
email='admin@timetrack.local',
is_admin=True,
is_verified=True, # Admin is automatically verified
role=Role.ADMIN,
two_factor_enabled=False
@@ -247,10 +246,7 @@ def migrate_database():
for user in users_to_update:
updated = False
if not hasattr(user, 'role') or user.role is None:
if user.is_admin:
user.role = Role.ADMIN
else:
user.role = Role.TEAM_MEMBER
user.role = Role.TEAM_MEMBER
updated = True
if not hasattr(user, 'two_factor_enabled') or user.two_factor_enabled is None:
user.two_factor_enabled = False

View File

@@ -61,9 +61,7 @@ def migrate_projects():
existing_projects = Project.query.count()
if existing_projects == 0:
# Find an admin or supervisor user to be the creator
admin_user = User.query.filter_by(is_admin=True).first()
if not admin_user:
admin_user = User.query.filter(User.role.in_([Role.ADMIN, Role.SUPERVISOR])).first()
admin_user = User.query.filter(User.role.in_([Role.ADMIN, Role.SUPERVISOR])).first()
if admin_user:
# Create some sample projects

View File

@@ -73,8 +73,8 @@ def migrate_roles_teams():
# Try to map the string to an enum value
user.role = role_mapping.get(user.role, Role.TEAM_MEMBER)
elif user.role is None:
# Set default role based on admin status
user.role = Role.ADMIN if user.is_admin else Role.TEAM_MEMBER
# Set default role
user.role = Role.TEAM_MEMBER
db.session.commit()
logger.info(f"Assigned {len(users)} existing users to default team and updated roles")

View File

@@ -13,16 +13,49 @@ class Role(enum.Enum):
SUPERVISOR = "Supervisor"
ADMIN = "Administrator" # Keep existing admin role
# Company model for multi-tenancy
class Company(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
slug = db.Column(db.String(50), unique=True, nullable=False) # URL-friendly identifier
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.now)
# Company settings
is_active = db.Column(db.Boolean, default=True)
max_users = db.Column(db.Integer, default=100) # Optional user limit
# Relationships
users = db.relationship('User', backref='company', lazy=True)
teams = db.relationship('Team', backref='company', lazy=True)
projects = db.relationship('Project', backref='company', lazy=True)
def __repr__(self):
return f'<Company {self.name}>'
def generate_slug(self):
"""Generate URL-friendly slug from company name"""
import re
slug = re.sub(r'[^\w\s-]', '', self.name.lower())
slug = re.sub(r'[-\s]+', '-', slug)
return slug.strip('-')
# Create Team model
class Team(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.String(255))
created_at = db.Column(db.DateTime, default=datetime.now)
# Company association for multi-tenancy
company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=False)
# Relationship with users (one team has many users)
users = db.relationship('User', backref='team', lazy=True)
# Unique constraint per company
__table_args__ = (db.UniqueConstraint('company_id', 'name', name='uq_team_name_per_company'),)
def __repr__(self):
return f'<Team {self.name}>'
@@ -30,11 +63,14 @@ class Project(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text, nullable=True)
code = db.Column(db.String(20), unique=True, nullable=False) # Project code (e.g., PRJ001)
code = db.Column(db.String(20), nullable=False) # Project code (e.g., PRJ001)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
# Company association for multi-tenancy
company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=False)
# Foreign key to user who created the project (Admin/Supervisor)
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
@@ -50,6 +86,9 @@ class Project(db.Model):
team = db.relationship('Team', backref='projects')
time_entries = db.relationship('TimeEntry', backref='project', lazy=True)
# Unique constraint per company
__table_args__ = (db.UniqueConstraint('company_id', 'code', name='uq_project_code_per_company'),)
def __repr__(self):
return f'<Project {self.code}: {self.name}>'
@@ -58,7 +97,11 @@ class Project(db.Model):
if not self.is_active:
return False
# Admins and Supervisors can log time to any project
# Must be in same company
if self.company_id != user.company_id:
return False
# Admins and Supervisors can log time to any project in their company
if user.role in [Role.ADMIN, Role.SUPERVISOR]:
return True
@@ -66,18 +109,20 @@ class Project(db.Model):
if self.team_id:
return user.team_id == self.team_id
# If no team restriction, any user can log time
# If no team restriction, any user in the company can log time
return True
# Update User model to include role and team relationship
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
username = db.Column(db.String(80), nullable=False)
email = db.Column(db.String(120), nullable=False)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# Company association for multi-tenancy
company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=False)
# Email verification fields
is_verified = db.Column(db.Boolean, default=False)
verification_token = db.Column(db.String(100), unique=True, nullable=True)
@@ -90,6 +135,12 @@ class User(db.Model):
role = db.Column(db.Enum(Role), default=Role.TEAM_MEMBER)
team_id = db.Column(db.Integer, db.ForeignKey('team.id'), nullable=True)
# Unique constraints per company
__table_args__ = (
db.UniqueConstraint('company_id', 'username', name='uq_user_username_per_company'),
db.UniqueConstraint('company_id', 'email', name='uq_user_email_per_company'),
)
# Two-Factor Authentication fields
two_factor_enabled = db.Column(db.Boolean, default=False)
two_factor_secret = db.Column(db.String(32), nullable=True) # Base32 encoded secret

View File

@@ -32,7 +32,7 @@ def repair_user_roles():
user.role = role_mapping.get(user.role, Role.TEAM_MEMBER)
fixed_count += 1
elif user.role is None:
user.role = Role.ADMIN if user.is_admin else Role.TEAM_MEMBER
user.role = Role.TEAM_MEMBER
fixed_count += 1
if fixed_count > 0:

View File

@@ -0,0 +1,213 @@
{% extends "layout.html" %}
{% block content %}
<div class="admin-container">
<div class="admin-header">
<h1>Company Management</h1>
<div class="admin-actions">
<a href="{{ url_for('setup_company') }}" class="btn btn-success">Create New Company</a>
<a href="{{ url_for('edit_company') }}" class="btn btn-primary">Edit Company</a>
</div>
</div>
<!-- Company Information Section -->
<div class="admin-section">
<h2>Company Information</h2>
<div class="company-info-grid">
<div class="info-card">
<div class="info-header">
<h3>{{ company.name }}</h3>
<span class="status-badge {% if company.is_active %}status-active{% else %}status-blocked{% endif %}">
{{ 'Active' if company.is_active else 'Inactive' }}
</span>
</div>
<div class="info-details">
<div class="detail-row">
<span class="detail-label">Company Code:</span>
<span class="detail-value">{{ company.slug }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Created:</span>
<span class="detail-value">{{ company.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Max Users:</span>
<span class="detail-value">{{ company.max_users or 'Unlimited' }}</span>
</div>
{% if company.description %}
<div class="detail-row">
<span class="detail-label">Description:</span>
<span class="detail-value">{{ company.description }}</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Statistics Section -->
<div class="stats-section">
<h2>Company Statistics</h2>
<div class="stats-grid">
<div class="stat-card">
<h3>{{ stats.total_users }}</h3>
<p>Total Users</p>
</div>
<div class="stat-card">
<h3>{{ stats.total_teams }}</h3>
<p>Teams</p>
</div>
<div class="stat-card">
<h3>{{ stats.total_projects }}</h3>
<p>Total Projects</p>
</div>
<div class="stat-card">
<h3>{{ stats.active_projects }}</h3>
<p>Active Projects</p>
</div>
</div>
</div>
<!-- Management Actions -->
<div class="admin-section">
<h2>Management</h2>
<div class="admin-panel">
<div class="admin-card">
<h3>Users</h3>
<p>Manage user accounts, roles, and permissions within your company.</p>
<a href="{{ url_for('company_users') }}" class="btn btn-secondary">Manage Users</a>
</div>
<div class="admin-card">
<h3>Teams</h3>
<p>Create and manage teams to organize your company structure.</p>
<a href="{{ url_for('admin_teams') }}" class="btn btn-secondary">Manage Teams</a>
</div>
<div class="admin-card">
<h3>Projects</h3>
<p>Set up and manage projects for time tracking and organization.</p>
<a href="{{ url_for('admin_projects') }}" class="btn btn-secondary">Manage Projects</a>
</div>
<div class="admin-card">
<h3>Settings</h3>
<p>Configure system-wide settings and preferences.</p>
<a href="{{ url_for('admin_settings') }}" class="btn btn-secondary">System Settings</a>
</div>
</div>
</div>
<!-- Company Code Section -->
<div class="admin-section">
<h2>User Registration</h2>
<div class="registration-info">
<p>Share this company code with new users for registration:</p>
<div class="code-display">
<input type="text" value="{{ company.slug }}" readonly id="companyCode" class="code-input">
<button class="btn btn-primary" onclick="copyToClipboard()">Copy Code</button>
</div>
<p class="help-text">New users will need this code when registering for your company.</p>
</div>
</div>
</div>
<script>
function copyToClipboard() {
const codeInput = document.getElementById('companyCode');
codeInput.select();
codeInput.setSelectionRange(0, 99999);
document.execCommand('copy');
// Show feedback
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Copied!';
button.classList.add('btn-success');
button.classList.remove('btn-primary');
setTimeout(() => {
button.textContent = originalText;
button.classList.remove('btn-success');
button.classList.add('btn-primary');
}, 2000);
}
</script>
<style>
.company-info-grid {
display: grid;
gap: 20px;
margin-bottom: 20px;
}
.info-card {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
}
.info-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
border-bottom: 1px solid #dee2e6;
padding-bottom: 10px;
}
.info-header h3 {
margin: 0;
color: #333;
}
.info-details {
display: grid;
gap: 8px;
}
.detail-row {
display: flex;
justify-content: space-between;
}
.detail-label {
font-weight: 600;
color: #666;
}
.detail-value {
color: #333;
}
.registration-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
}
.code-display {
display: flex;
gap: 10px;
margin: 15px 0;
}
.code-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
font-family: monospace;
font-size: 14px;
}
.help-text {
margin: 0;
color: #666;
font-size: 14px;
}
</style>
{% endblock %}

View File

@@ -24,7 +24,7 @@
<tr>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{% if user.is_admin %}Admin{% else %}User{% endif %}</td>
<td>{{ user.role.value if user.role else 'Team Member' }}</td>
<td>
<span class="status-badge {% if user.is_blocked %}status-blocked{% else %}status-active{% endif %}">
{% if user.is_blocked %}Blocked{% else %}Active{% endif %}

View File

@@ -0,0 +1,202 @@
{% extends "layout.html" %}
{% block content %}
<div class="admin-container">
<div class="admin-header">
<h1>Company Users - {{ company.name }}</h1>
<a href="{{ url_for('create_user') }}" class="btn btn-success">Create New User</a>
</div>
<!-- User Statistics -->
<div class="stats-section">
<h2>User Statistics</h2>
<div class="stats-grid">
<div class="stat-card">
<h3>{{ stats.total }}</h3>
<p>Total Users</p>
</div>
<div class="stat-card">
<h3>{{ stats.active }}</h3>
<p>Active Users</p>
</div>
<div class="stat-card">
<h3>{{ stats.unverified }}</h3>
<p>Unverified</p>
</div>
<div class="stat-card">
<h3>{{ stats.blocked }}</h3>
<p>Blocked</p>
</div>
<div class="stat-card">
<h3>{{ stats.admins }}</h3>
<p>Administrators</p>
</div>
<div class="stat-card">
<h3>{{ stats.supervisors }}</h3>
<p>Supervisors</p>
</div>
</div>
</div>
<!-- User List -->
<div class="admin-section">
<h2>User List</h2>
{% if users %}
<div class="user-list">
<table class="data-table">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Team</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>
{{ user.username }}
{% if user.two_factor_enabled %}
<span class="security-badge" title="2FA Enabled">🔒</span>
{% endif %}
</td>
<td>{{ user.email }}</td>
<td>
<span class="role-badge role-{{ user.role.name.lower() }}">
{{ user.role.value }}
</span>
</td>
<td>
{% if user.team %}
<span class="team-badge">{{ user.team.name }}</span>
{% else %}
<span class="text-muted">No team</span>
{% endif %}
</td>
<td>
<span class="status-badge {% if user.is_blocked %}status-blocked{% elif not user.is_verified %}status-unverified{% else %}status-active{% endif %}">
{% if user.is_blocked %}Blocked{% elif not user.is_verified %}Unverified{% else %}Active{% endif %}
</span>
</td>
<td>{{ user.created_at.strftime('%Y-%m-%d') }}</td>
<td>
<a href="{{ url_for('edit_user', user_id=user.id) }}" class="btn btn-sm btn-primary">Edit</a>
{% if user.id != g.user.id %}
{% if user.is_blocked %}
<a href="{{ url_for('toggle_user_status', user_id=user.id) }}" class="btn btn-sm btn-success">Unblock</a>
{% else %}
<a href="{{ url_for('toggle_user_status', user_id=user.id) }}" class="btn btn-sm btn-warning">Block</a>
{% endif %}
<button class="btn btn-sm btn-danger" onclick="confirmDelete({{ user.id }}, '{{ user.username }}')">Delete</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<h3>No Users Found</h3>
<p>There are no users in this company yet.</p>
<a href="{{ url_for('create_user') }}" class="btn btn-primary">Add First User</a>
</div>
{% endif %}
</div>
<!-- Navigation -->
<div class="admin-section">
<a href="{{ url_for('admin_company') }}" class="btn btn-secondary">← Back to Company Management</a>
</div>
</div>
<script>
function confirmDelete(userId, username) {
if (confirm(`Are you sure you want to delete user "${username}"? This action cannot be undone.`)) {
fetch(`/admin/users/delete/${userId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
}).then(response => {
if (response.ok) {
location.reload();
} else {
alert('Error deleting user');
}
});
}
}
</script>
<style>
.security-badge {
font-size: 12px;
margin-left: 5px;
}
.role-badge {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
display: inline-block;
}
.role-admin {
background-color: #ff6b6b;
color: white;
}
.role-supervisor {
background-color: #ffa726;
color: white;
}
.role-team_leader {
background-color: #42a5f5;
color: white;
}
.role-team_member {
background-color: #66bb6a;
color: white;
}
.team-badge {
background-color: #e3f2fd;
color: #1976d2;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
border: 1px solid #bbdefb;
}
.status-unverified {
background-color: #fff3cd;
color: #856404;
}
.empty-state {
text-align: center;
padding: 40px 20px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
}
.empty-state h3 {
color: #666;
margin-bottom: 10px;
}
.empty-state p {
color: #888;
margin-bottom: 20px;
}
</style>
{% endblock %}

View File

@@ -21,10 +21,22 @@
</div>
<div class="form-group">
<label class="checkbox-container">
<input type="checkbox" name="is_admin"> Administrator privileges
<span class="checkmark"></span>
</label>
<label for="role">Role</label>
<select id="role" name="role" class="form-control">
{% for role in roles %}
<option value="{{ role.name }}">{{ role.value }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="team_id">Team (Optional)</label>
<select id="team_id" name="team_id" class="form-control">
<option value="">No Team</option>
{% for team in teams %}
<option value="{{ team.id }}">{{ team.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">

View File

@@ -3,7 +3,7 @@
{% block content %}
<div class="admin-container">
<h1>
{% if g.user.is_admin or g.user.role == Role.ADMIN %}
{% if g.user.role == Role.ADMIN %}
Admin Dashboard
{% elif g.user.role == Role.SUPERVISOR %}
Supervisor Dashboard
@@ -39,7 +39,7 @@
</div>
<!-- Admin-only sections -->
{% if g.user.is_admin or g.user.role == Role.ADMIN %}
{% if g.user.role == Role.ADMIN %}
<div class="stats-section">
<h2>System Overview</h2>
<div class="stats-grid">
@@ -90,7 +90,7 @@
{% endif %}
<!-- Team Leader and Supervisor sections -->
{% if g.user.role in [Role.TEAM_LEADER, Role.SUPERVISOR] or g.user.is_admin %}
{% if g.user.role in [Role.TEAM_LEADER, Role.SUPERVISOR, Role.ADMIN] %}
<div class="team-section">
<h2>Team Management</h2>
@@ -113,7 +113,7 @@
<a href="{{ url_for('team_hours') }}" class="btn btn-primary">View Team Hours</a>
</div>
{% if g.user.is_admin %}
{% if g.user.role == Role.ADMIN %}
<div class="admin-card">
<h2>Team Configuration</h2>
<p>Create and manage team structures.</p>
@@ -161,7 +161,7 @@
<table class="time-history">
<thead>
<tr>
{% if g.user.is_admin or g.user.role in [Role.TEAM_LEADER, Role.SUPERVISOR] %}
{% if g.user.role in [Role.ADMIN, Role.TEAM_LEADER, Role.SUPERVISOR] %}
<th>User</th>
{% endif %}
<th>Date</th>
@@ -174,7 +174,7 @@
<tbody>
{% for entry in recent_entries %}
<tr>
{% if g.user.is_admin or g.user.role in [Role.TEAM_LEADER, Role.SUPERVISOR] %}
{% if g.user.role in [Role.ADMIN, Role.TEAM_LEADER, Role.SUPERVISOR] %}
<td>{{ entry.user.username }}</td>
{% endif %}
<td>{{ entry.arrival_time.strftime('%Y-%m-%d') }}</td>

View File

@@ -0,0 +1,73 @@
{% extends "layout.html" %}
{% block content %}
<div class="admin-container">
<h1>Edit Company</h1>
<form method="POST" class="user-form">
<div class="form-group">
<label for="name">Company Name</label>
<input type="text" id="name" name="name" class="form-control"
value="{{ company.name }}" required autofocus>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" class="form-control"
rows="3">{{ company.description or '' }}</textarea>
<small class="form-help">Optional description of your company</small>
</div>
<div class="form-group">
<label for="max_users">Maximum Users</label>
<input type="number" id="max_users" name="max_users" class="form-control"
value="{{ company.max_users or '' }}" min="1">
<small class="form-help">Leave empty for unlimited users</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="is_active" name="is_active"
{{ 'checked' if company.is_active else '' }}>
Company is active
</label>
<small class="form-help">Inactive companies cannot be accessed by users</small>
</div>
<div class="info-box">
<h3>Company Code</h3>
<p><strong>{{ company.slug }}</strong></p>
<p>This code cannot be changed and is used by new users to register for your company.</p>
</div>
<div class="form-actions">
<a href="{{ url_for('admin_company') }}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
<style>
.info-box {
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.info-box h3 {
margin-top: 0;
color: #0066cc;
}
.info-box p {
margin-bottom: 10px;
}
.info-box p:last-child {
margin-bottom: 0;
color: #666;
}
</style>
{% endblock %}

View File

@@ -43,12 +43,6 @@
</select>
</div>
<div class="form-group">
<label class="checkbox-container">
<input type="checkbox" name="is_admin" {% if user.is_admin %}checked{% endif %}> Administrator privileges
<span class="checkmark"></span>
</label>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Update User</button>

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }} - TimeTrack</title>
<title>{{ title }} - TimeTrack{% if g.company %} - {{ g.company.name }}{% endif %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
@@ -11,6 +11,9 @@
<header class="mobile-header">
<div class="mobile-nav-brand">
<a href="{{ url_for('home') }}">TimeTrack</a>
{% if g.company %}
<small class="company-name">{{ g.company.name }}</small>
{% endif %}
</div>
<button class="mobile-nav-toggle" id="mobile-nav-toggle">
<span></span>
@@ -23,6 +26,11 @@
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<h2><a href="{{ url_for('home') }}">TimeTrack</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">
<span></span>
<span></span>
@@ -36,13 +44,14 @@
<li><a href="{{ url_for('history') }}" data-tooltip="History"><i class="nav-icon">📊</i><span class="nav-text">History</span></a></li>
<!-- Role-based menu items -->
{% if g.user.is_admin %}
{% if g.user.role == Role.ADMIN %}
<li class="nav-divider">Admin</li>
<li><a href="{{ url_for('profile') }}" data-tooltip="Profile"><i class="nav-icon">👤</i><span class="nav-text">Profile</span></a></li>
<li><a href="{{ url_for('config') }}" data-tooltip="Config"><i class="nav-icon">⚙️</i><span class="nav-text">Config</span></a></li>
<li><a href="{{ url_for('dashboard') }}" data-tooltip="Dashboard"><i class="nav-icon">📈</i><span class="nav-text">Dashboard</span></a></li>
<li><a href="{{ url_for('admin_company') }}" data-tooltip="Company"><i class="nav-icon">🏢</i><span class="nav-text">Company</span></a></li>
<li><a href="{{ url_for('admin_users') }}" data-tooltip="Manage Users"><i class="nav-icon">👥</i><span class="nav-text">Manage Users</span></a></li>
<li><a href="{{ url_for('admin_teams') }}" data-tooltip="Manage Teams"><i class="nav-icon">🏢</i><span class="nav-text">Manage Teams</span></a></li>
<li><a href="{{ url_for('admin_teams') }}" data-tooltip="Manage Teams"><i class="nav-icon">🏭</i><span class="nav-text">Manage Teams</span></a></li>
<li><a href="{{ url_for('admin_projects') }}" data-tooltip="Manage Projects"><i class="nav-icon">📝</i><span class="nav-text">Manage Projects</span></a></li>
<li><a href="{{ url_for('admin_settings') }}" data-tooltip="System Settings"><i class="nav-icon">🔧</i><span class="nav-text">System Settings</span></a></li>
{% if g.user.team_id %}

View File

@@ -14,7 +14,7 @@
<div class="profile-info">
<p><strong>Username:</strong> {{ user.username }}</p>
<p><strong>Account Type:</strong> {% if user.is_admin %}Administrator{% else %}User{% endif %}</p>
<p><strong>Account Type:</strong> {{ user.role.value if user.role else 'Team Member' }}</p>
<p><strong>Member Since:</strong> {{ user.created_at.strftime('%Y-%m-%d') }}</p>
<p><strong>Two-Factor Authentication:</strong>
{% if user.two_factor_enabled %}

View File

@@ -13,9 +13,16 @@
{% endwith %}
<form method="POST" action="{{ url_for('register') }}" class="auth-form">
<div class="form-group">
<label for="company_code">Company Code</label>
<input type="text" id="company_code" name="company_code" class="form-control" required autofocus
placeholder="Enter your company code">
<small class="form-text text-muted">Get this code from your company administrator.</small>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control" required autofocus>
<input type="text" id="username" name="username" class="form-control" required>
</div>
<div class="form-group">

View File

@@ -0,0 +1,261 @@
{% extends "layout.html" %}
{% block content %}
<div class="admin-container">
<h1>
{% if is_initial_setup %}
Welcome to TimeTrack
{% else %}
Create New Company
{% endif %}
</h1>
<!-- Info Message -->
{% if is_initial_setup %}
<div class="info-message">
<h3>🎉 Let's Get Started!</h3>
<p>Set up your company and create the first administrator account to begin using TimeTrack.</p>
</div>
{% elif is_super_admin %}
<div class="info-message">
<h3>🏢 New Company Setup</h3>
<p>Create a new company with its own administrator. This will be a separate organization within TimeTrack.</p>
</div>
{% else %}
<div class="error-message">
<h3>⚠️ Access Denied</h3>
<p>You do not have permission to create new companies.</p>
<a href="{{ url_for('home') }}" class="btn btn-secondary">Return Home</a>
</div>
{% set show_form = false %}
{% endif %}
{% if is_initial_setup or is_super_admin %}
{% set show_form = true %}
{% endif %}
{% if show_form %}
<form method="POST" class="company-setup-form">
<!-- Company Information Section -->
<div class="form-section">
<h2>Company Information</h2>
<div class="form-group">
<label for="company_name">Company Name</label>
<input type="text" id="company_name" name="company_name" class="form-control"
value="{{ request.form.company_name or '' }}" required autofocus
placeholder="e.g., Acme Corporation">
<small class="form-help">This will be displayed throughout the application</small>
</div>
<div class="form-group">
<label for="company_description">Description (Optional)</label>
<textarea id="company_description" name="company_description" class="form-control"
rows="3" placeholder="Brief description of your company">{{ request.form.company_description or '' }}</textarea>
</div>
</div>
<!-- Administrator Account Section -->
<div class="form-section">
<h2>Administrator Account</h2>
<div class="form-row">
<div class="form-group">
<label for="admin_username">Username</label>
<input type="text" id="admin_username" name="admin_username" class="form-control"
value="{{ request.form.admin_username or '' }}" required>
</div>
<div class="form-group">
<label for="admin_email">Email</label>
<input type="email" id="admin_email" name="admin_email" class="form-control"
value="{{ request.form.admin_email or '' }}" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="admin_password">Password</label>
<input type="password" id="admin_password" name="admin_password" class="form-control" required>
<small class="form-help">Minimum 6 characters</small>
</div>
<div class="form-group">
<label for="confirm_password">Confirm Password</label>
<input type="password" id="confirm_password" name="confirm_password" class="form-control" required>
</div>
</div>
<div class="admin-info">
<h3>Administrator Privileges</h3>
<p>
{% if is_initial_setup %}
This account will have full access to manage users, teams, and projects within your company.
{% else %}
This administrator will have full control over the new company and its users, teams, and projects.
{% endif %}
</p>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
{% if is_super_admin %}
<a href="{{ url_for('admin_company') }}" class="btn btn-secondary">
← Back to Dashboard
</a>
{% endif %}
<button type="submit" class="btn btn-success">
🚀 {% if is_initial_setup %}Create Company & Admin Account{% else %}Create New Company{% endif %}
</button>
</div>
</form>
{% if is_initial_setup and existing_companies > 0 %}
<div class="alternative-actions">
<p>Already have an account?</p>
<a href="{{ url_for('login') }}" class="btn btn-secondary">Go to Login</a>
</div>
{% endif %}
{% endif %}
</div>
<script>
// Form validation
function validatePasswords() {
const password = document.getElementById('admin_password').value;
const confirmPassword = document.getElementById('confirm_password').value;
const confirmField = document.getElementById('confirm_password');
if (password && confirmPassword && password !== confirmPassword) {
confirmField.setCustomValidity('Passwords do not match');
return false;
} else {
confirmField.setCustomValidity('');
return true;
}
}
document.getElementById('confirm_password').addEventListener('input', validatePasswords);
document.getElementById('admin_password').addEventListener('input', validatePasswords);
// Form submission validation
document.querySelector('.company-setup-form').addEventListener('submit', function(e) {
if (!validatePasswords()) {
e.preventDefault();
alert('Please ensure passwords match before submitting.');
}
});
</script>
<style>
.company-setup-form {
max-width: 800px;
margin: 0 auto;
}
.form-section {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 30px;
margin-bottom: 30px;
}
.form-section h2 {
margin-top: 0;
margin-bottom: 20px;
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
}
.info-message {
background: linear-gradient(135deg, #e3f2fd, #f3e5f5);
border: 1px solid #b3d9ff;
border-radius: 12px;
padding: 30px;
margin-bottom: 30px;
text-align: center;
}
.info-message h3 {
margin-top: 0;
color: #0066cc;
font-size: 24px;
}
.info-message p {
margin-bottom: 0;
color: #555;
font-size: 16px;
}
.error-message {
background: #fff5f5;
border: 1px solid #fed7d7;
border-radius: 12px;
padding: 30px;
margin-bottom: 30px;
text-align: center;
}
.error-message h3 {
margin-top: 0;
color: #c53030;
font-size: 24px;
}
.error-message p {
margin-bottom: 20px;
color: #555;
font-size: 16px;
}
.admin-info {
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
}
.admin-info h3 {
margin-top: 0;
margin-bottom: 10px;
color: #0066cc;
font-size: 16px;
}
.admin-info p {
margin-bottom: 0;
color: #555;
line-height: 1.5;
}
.alternative-actions {
text-align: center;
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.alternative-actions p {
margin-bottom: 15px;
color: #666;
}
</style>
{% endblock %}