Merge pull request #7 from nullmedium/company-feature
Add company feature.
This commit is contained in:
@@ -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,9 +246,6 @@ 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
|
||||
updated = True
|
||||
if not hasattr(user, 'two_factor_enabled') or user.two_factor_enabled is None:
|
||||
|
||||
@@ -61,8 +61,6 @@ 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()
|
||||
|
||||
if admin_user:
|
||||
|
||||
@@ -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")
|
||||
|
||||
65
models.py
65
models.py
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
213
templates/admin_company.html
Normal file
213
templates/admin_company.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
|
||||
202
templates/company_users.html
Normal file
202
templates/company_users.html
Normal 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 %}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
73
templates/edit_company.html
Normal file
73
templates/edit_company.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
261
templates/setup_company.html
Normal file
261
templates/setup_company.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user