Merge pull request #7 from nullmedium/company-feature

Add company feature.
This commit is contained in:
Jens Luedicke
2025-07-02 12:43:08 +02:00
committed by GitHub
17 changed files with 1465 additions and 216 deletions

785
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 %}