Merge branch 'master' into improve-history-views
This commit is contained in:
36
.dockerignore
Normal file
36
.dockerignore
Normal file
@@ -0,0 +1,36 @@
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.DS_Store
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
env
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
.tox
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.log
|
||||
.venv
|
||||
.mypy_cache
|
||||
.pytest_cache
|
||||
.hypothesis
|
||||
fly.toml
|
||||
timetrack.db
|
||||
*.db-journal
|
||||
tests/
|
||||
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
FLASK_APP=app.py
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements file first for better caching
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install Python dependencies
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy the rest of the application
|
||||
COPY . .
|
||||
|
||||
# Create the SQLite database directory with proper permissions
|
||||
RUN mkdir -p /app/instance && chmod 777 /app/instance
|
||||
|
||||
VOLUME /data
|
||||
RUN mkdir /data && chmod 777 /data
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 5000
|
||||
|
||||
# Database will be created at runtime when /data volume is mounted
|
||||
|
||||
# Command to run the application
|
||||
CMD ["flask", "run", "--host=0.0.0.0", "--port=5000"]
|
||||
42
README.md
42
README.md
@@ -65,31 +65,37 @@ pipenv install
|
||||
# Activate the virtual environment
|
||||
pipenv shell
|
||||
|
||||
# Initialize the database and run migrations
|
||||
python migrate_db.py
|
||||
python migrate_roles_teams.py # Add role and team support
|
||||
python migrate_projects.py # Add project management
|
||||
|
||||
# Run the application
|
||||
# Run the application (migrations run automatically on first startup)
|
||||
python app.py
|
||||
```
|
||||
|
||||
### First-Time Setup
|
||||
|
||||
1. **Admin Account**: Create the first admin user through the registration page
|
||||
2. **System Configuration**: Access Admin Dashboard to configure system settings
|
||||
3. **Team Setup**: Create teams and assign team leaders
|
||||
4. **Project Creation**: Set up projects with codes and team assignments
|
||||
5. **User Management**: Add users and assign appropriate roles
|
||||
1. **Start the Application**: The database is automatically created and initialized on first startup
|
||||
2. **Admin Account**: An initial admin user is created automatically with username `admin` and password `admin`
|
||||
3. **Change Default Password**: **IMPORTANT**: Change the default admin password immediately after first login
|
||||
4. **System Configuration**: Access Admin Dashboard to configure system settings
|
||||
5. **Team Setup**: Create teams and assign team leaders
|
||||
6. **Project Creation**: Set up projects with codes and team assignments
|
||||
7. **User Management**: Add users and assign appropriate roles
|
||||
|
||||
### Database Migrations
|
||||
|
||||
The application includes several migration scripts to upgrade existing installations:
|
||||
**Automatic Migration System**: All database migrations now run automatically when the application starts. No manual migration scripts need to be run.
|
||||
|
||||
- `migrate_db.py`: Core database initialization
|
||||
- `migrate_roles_teams.py`: Add role-based access control and team management
|
||||
- `migrate_projects.py`: Add project management capabilities
|
||||
- `repair_roles.py`: Fix role assignments if needed
|
||||
The integrated migration system handles:
|
||||
- Database schema creation for new installations
|
||||
- Automatic schema updates for existing databases
|
||||
- User table enhancements (verification, roles, teams, 2FA)
|
||||
- Project and team management table creation
|
||||
- Sample data initialization
|
||||
- Data integrity maintenance during upgrades
|
||||
|
||||
**Legacy Migration Files**: The following files are maintained for reference but are no longer needed:
|
||||
- `migrate_db.py`: Legacy core database migration (now integrated)
|
||||
- `migrate_roles_teams.py`: Legacy role and team migration (now integrated)
|
||||
- `migrate_projects.py`: Legacy project migration (now integrated)
|
||||
- `repair_roles.py`: Legacy role repair utility (functionality now integrated)
|
||||
|
||||
### Configuration
|
||||
|
||||
@@ -139,11 +145,11 @@ The application provides various endpoints for different user roles:
|
||||
|
||||
## File Structure
|
||||
|
||||
- `app.py`: Main Flask application
|
||||
- `app.py`: Main Flask application with integrated migration system
|
||||
- `models.py`: Database models and relationships
|
||||
- `templates/`: HTML templates for all pages
|
||||
- `static/`: CSS and JavaScript files
|
||||
- `migrate_*.py`: Database migration scripts
|
||||
- `migrate_*.py`: Legacy migration scripts (no longer needed)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
34
fly.toml
Normal file
34
fly.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
# fly.toml app configuration file generated for timetrack-2whuug on 2025-07-01T09:27:14Z
|
||||
#
|
||||
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
|
||||
#
|
||||
|
||||
app = 'timetrack-2whuug'
|
||||
primary_region = 'fra'
|
||||
|
||||
[build]
|
||||
|
||||
[http_service]
|
||||
internal_port = 5000
|
||||
force_https = true
|
||||
auto_stop_machines = 'stop'
|
||||
auto_start_machines = true
|
||||
min_machines_running = 0
|
||||
processes = ['app']
|
||||
|
||||
[env]
|
||||
MAIL_SERVER = "smtp.ionos.de"
|
||||
MAIL_PORT = 587
|
||||
MAIL_USE_TLS = 1
|
||||
MAIL_USERNAME = "jens@luedicke.cloud"
|
||||
MAIL_DEFAULT_SENDER = "jens@luedicke.cloud"
|
||||
|
||||
|
||||
[mounts]
|
||||
source = "timetrack_data"
|
||||
destination = "/data"
|
||||
|
||||
[[vm]]
|
||||
cpu_kind = 'shared'
|
||||
cpus = 1
|
||||
memory_mb = 256
|
||||
@@ -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
|
||||
@@ -317,6 +313,19 @@ def init_system_settings():
|
||||
db.session.commit()
|
||||
print("Registration setting initialized to enabled")
|
||||
|
||||
# Check if email_verification_required setting exists
|
||||
email_verification_setting = SystemSettings.query.filter_by(key='email_verification_required').first()
|
||||
if not email_verification_setting:
|
||||
print("Adding email_verification_required system setting...")
|
||||
email_verification_setting = SystemSettings(
|
||||
key='email_verification_required',
|
||||
value='true', # Default to enabled for security
|
||||
description='Controls whether email verification is required for new user accounts'
|
||||
)
|
||||
db.session.add(email_verification_setting)
|
||||
db.session.commit()
|
||||
print("Email verification setting initialized to enabled")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate_database()
|
||||
print("Database migration completed")
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
@@ -20,7 +20,17 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- You can add more settings here in the future -->
|
||||
<div class="form-group">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" name="email_verification_required"
|
||||
{% if settings.email_verification_required %}checked{% endif %}>
|
||||
<span class="checkmark"></span>
|
||||
Require Email Verification
|
||||
</label>
|
||||
<p class="setting-description">
|
||||
When enabled, new users must verify their email address before accessing the application. When disabled, new users can log in immediately after registration.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -107,8 +107,7 @@
|
||||
</div>
|
||||
|
||||
<div class="admin-panel">
|
||||
|
||||
{% 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>
|
||||
@@ -156,7 +155,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>
|
||||
@@ -169,7 +168,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('analytics') }}" data-tooltip="Time Analytics"><i class="nav-icon">📊</i><span class="nav-text">Analytics</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>
|
||||
{% elif g.user.role in [Role.TEAM_LEADER, Role.SUPERVISOR] %}
|
||||
|
||||
@@ -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