Implement comprehensive project time logging feature

Add complete project management system with role-based access control:

**Core Features:**
- Project creation and management for Admins/Supervisors
- Time tracking with optional project selection and notes
- Project-based filtering and reporting in history
- Enhanced export functionality with project data
- Team-specific project assignments

**Database Changes:**
- New Project model with full relationships
- Enhanced TimeEntry model with project_id and notes
- Updated migration scripts with rollback support
- Sample project creation for testing

**User Interface:**
- Project management templates (create, edit, list)
- Enhanced time tracking with project dropdown
- Project filtering in history page
- Updated navigation for role-based access
- Modern styling with hover effects and responsive design

**API Enhancements:**
- Project validation and access control
- Updated arrive endpoint with project support
- Enhanced export functions with project data
- Role-based route protection

**Migration Support:**
- Comprehensive migration scripts (migrate_projects.py)
- Updated main migration script (migrate_db.py)
- Detailed migration documentation
- Rollback functionality for safe deployment

**Role-Based Access:**
- Admins: Full project CRUD operations
- Supervisors: Project creation and management
- Team Leaders: View team hours with projects
- Team Members: Select projects when tracking time

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jens Luedicke
2025-06-29 17:18:10 +02:00
parent 77d26a6063
commit be111a4bed
12 changed files with 1393 additions and 14 deletions

174
MIGRATION_PROJECTS.md Normal file
View File

@@ -0,0 +1,174 @@
# Project Time Logging Migration Guide
This document explains how to migrate your TimeTrack database to support the new Project Time Logging feature.
## Overview
The Project Time Logging feature adds the ability to:
- Track time against specific projects
- Manage projects with role-based access control
- Filter and report on project-based time entries
- Export data with project information
## Database Changes
### New Tables
- **`project`**: Stores project information including name, code, description, team assignment, and dates
### Modified Tables
- **`time_entry`**: Added `project_id` (foreign key) and `notes` (text) columns
- **Existing data**: All existing time entries remain unchanged and will show as "No project assigned"
## Migration Options
### Option 1: Run Main Migration Script (Recommended)
The main migration script has been updated to include project functionality:
```bash
python migrate_db.py
```
This will:
- Create the project table
- Add project_id and notes columns to time_entry
- Create 3 sample projects (if no admin user exists)
- Maintain all existing data
### Option 2: Run Project-Specific Migration
For existing installations, you can run the project-specific migration:
```bash
python migrate_projects.py
```
### Option 3: Manual Migration
If you prefer to handle the migration manually, execute these SQL commands:
```sql
-- Create project table
CREATE TABLE project (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
description TEXT,
code VARCHAR(20) NOT NULL UNIQUE,
is_active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by_id INTEGER NOT NULL,
team_id INTEGER,
start_date DATE,
end_date DATE,
FOREIGN KEY (created_by_id) REFERENCES user (id),
FOREIGN KEY (team_id) REFERENCES team (id)
);
-- Add columns to time_entry table
ALTER TABLE time_entry ADD COLUMN project_id INTEGER;
ALTER TABLE time_entry ADD COLUMN notes TEXT;
```
## Sample Projects
The migration creates these sample projects (if admin user exists):
1. **ADMIN001** - General Administration
2. **DEV001** - Development Project
3. **SUPPORT001** - Customer Support
These can be modified or deleted after migration.
## Rollback
To rollback the project functionality (removes projects but keeps time entry columns):
```bash
python migrate_projects.py rollback
```
**Note**: Due to SQLite limitations, the `project_id` and `notes` columns cannot be removed from the `time_entry` table during rollback.
## Post-Migration Steps
1. **Verify Migration**: Check that the migration completed successfully
2. **Create Projects**: Admin/Supervisor users can create projects via the web interface
3. **Assign Teams**: Optionally assign projects to specific teams
4. **User Training**: Inform users about the new project selection feature
## Migration Verification
After running the migration, verify it worked by:
1. **Check Tables**:
```sql
.tables -- Should show 'project' table
.schema project -- Verify project table structure
.schema time_entry -- Verify project_id and notes columns
```
2. **Check Web Interface**:
- Admin/Supervisor users should see "Manage Projects" in their dropdown menu
- Time tracking interface should show project selection dropdown
- History page should have project filtering
3. **Check Sample Projects**:
```sql
SELECT * FROM project; -- Should show 3 sample projects
```
## Troubleshooting
### Migration Fails
- Ensure no active connections to the database
- Check file permissions
- Verify admin user exists in the database
### Missing Navigation Links
- Clear browser cache
- Verify user has Admin or Supervisor role
- Check that the templates have been updated
### Project Selection Not Available
- Verify migration completed successfully
- Check that active projects exist in the database
- Ensure user has permission to access projects
## Feature Access
### Admin Users
- Create, edit, delete, and manage all projects
- Access project management interface
- View all project reports
### Supervisor Users
- Create, edit, and manage projects
- Access project management interface
- View project reports
### Team Leader Users
- View team hours with project breakdown
- No project creation/management access
### Team Member Users
- Select projects when tracking time
- View personal history with project filtering
- No project management access
## File Changes
The migration affects these files:
- `migrate_db.py` - Updated main migration script
- `migrate_projects.py` - New project-specific migration
- `models.py` - Added Project model and updated TimeEntry
- `app.py` - Added project routes and updated existing routes
- Templates - Updated with project functionality
- `static/js/script.js` - Updated time tracking JavaScript
## Backup Recommendation
Before running any migration, it's recommended to backup your database:
```bash
cp timetrack.db timetrack.db.backup
```
This allows you to restore the original database if needed.

221
app.py
View File

@@ -1,5 +1,5 @@
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project
import logging import logging
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
import os import os
@@ -154,7 +154,17 @@ def home():
TimeEntry.arrival_time <= datetime.combine(today, time.max) TimeEntry.arrival_time <= datetime.combine(today, time.max)
).order_by(TimeEntry.arrival_time.desc()).all() ).order_by(TimeEntry.arrival_time.desc()).all()
return render_template('index.html', title='Home', active_entry=active_entry, history=history) # Get available projects for this user
available_projects = []
all_projects = Project.query.filter_by(is_active=True).all()
for project in all_projects:
if project.is_user_allowed(g.user):
available_projects.append(project)
return render_template('index.html', title='Home',
active_entry=active_entry,
history=history,
available_projects=available_projects)
else: else:
return render_template('index.html', title='Home') return render_template('index.html', title='Home')
@@ -683,14 +693,35 @@ def timetrack():
@app.route('/api/arrive', methods=['POST']) @app.route('/api/arrive', methods=['POST'])
@login_required @login_required
def arrive(): def arrive():
# Get project and notes from request
project_id = request.json.get('project_id') if request.json else None
notes = request.json.get('notes') if request.json else None
# Validate project access if project is specified
if project_id:
project = Project.query.get(project_id)
if not project or not project.is_user_allowed(g.user):
return jsonify({'error': 'Invalid or unauthorized project'}), 403
# Create a new time entry with arrival time for the current user # Create a new time entry with arrival time for the current user
new_entry = TimeEntry(user_id=session['user_id'], arrival_time=datetime.now()) new_entry = TimeEntry(
user_id=g.user.id,
arrival_time=datetime.now(),
project_id=int(project_id) if project_id else None,
notes=notes
)
db.session.add(new_entry) db.session.add(new_entry)
db.session.commit() db.session.commit()
return jsonify({ return jsonify({
'id': new_entry.id, 'id': new_entry.id,
'arrival_time': new_entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S') 'arrival_time': new_entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S'),
'project': {
'id': new_entry.project.id,
'code': new_entry.project.code,
'name': new_entry.project.name
} if new_entry.project else None,
'notes': new_entry.notes
}) })
@app.route('/api/leave/<int:entry_id>', methods=['POST']) @app.route('/api/leave/<int:entry_id>', methods=['POST'])
@@ -900,10 +931,38 @@ def team_hours():
@app.route('/history') @app.route('/history')
@login_required @login_required
def history(): def history():
# Get all time entries for the current user, ordered by most recent first # Get project filter from query parameters
all_entries = TimeEntry.query.filter_by(user_id=session['user_id']).order_by(TimeEntry.arrival_time.desc()).all() project_filter = request.args.get('project_id')
return render_template('history.html', title='Time Entry History', entries=all_entries) # Base query for user's time entries
query = TimeEntry.query.filter_by(user_id=g.user.id)
# Apply project filter if specified
if project_filter:
if project_filter == 'none':
# Show entries with no project assigned
query = query.filter(TimeEntry.project_id.is_(None))
else:
# Show entries for specific project
try:
project_id = int(project_filter)
query = query.filter_by(project_id=project_id)
except ValueError:
# Invalid project ID, ignore filter
pass
# Get filtered entries ordered by most recent first
all_entries = query.order_by(TimeEntry.arrival_time.desc()).all()
# Get available projects for the filter dropdown
available_projects = []
all_projects = Project.query.filter_by(is_active=True).all()
for project in all_projects:
if project.is_user_allowed(g.user):
available_projects.append(project)
return render_template('history.html', title='Time Entry History',
entries=all_entries, available_projects=available_projects)
def calculate_work_duration(arrival_time, departure_time, total_break_duration): def calculate_work_duration(arrival_time, departure_time, total_break_duration):
""" """
@@ -1187,6 +1246,149 @@ def manage_team(team_id):
available_users=available_users available_users=available_users
) )
# Project Management Routes
@app.route('/admin/projects')
@role_required(Role.SUPERVISOR) # Supervisors and Admins can manage projects
def admin_projects():
projects = Project.query.order_by(Project.created_at.desc()).all()
return render_template('admin_projects.html', title='Project Management', projects=projects)
@app.route('/admin/projects/create', methods=['GET', 'POST'])
@role_required(Role.SUPERVISOR)
def create_project():
if request.method == 'POST':
name = request.form.get('name')
description = request.form.get('description')
code = request.form.get('code')
team_id = request.form.get('team_id') or None
start_date_str = request.form.get('start_date')
end_date_str = request.form.get('end_date')
# Validate input
error = None
if not name:
error = 'Project name is required'
elif not code:
error = 'Project code is required'
elif Project.query.filter_by(code=code).first():
error = 'Project code already exists'
# Parse dates
start_date = None
end_date = None
if start_date_str:
try:
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
except ValueError:
error = 'Invalid start date format'
if end_date_str:
try:
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
except ValueError:
error = 'Invalid end date format'
if start_date and end_date and start_date > end_date:
error = 'Start date cannot be after end date'
if error is None:
project = Project(
name=name,
description=description,
code=code.upper(),
team_id=int(team_id) if team_id else None,
start_date=start_date,
end_date=end_date,
created_by_id=g.user.id
)
db.session.add(project)
db.session.commit()
flash(f'Project "{name}" created successfully!', 'success')
return redirect(url_for('admin_projects'))
else:
flash(error, 'error')
# Get available teams for the form
teams = Team.query.order_by(Team.name).all()
return render_template('create_project.html', title='Create Project', teams=teams)
@app.route('/admin/projects/edit/<int:project_id>', methods=['GET', 'POST'])
@role_required(Role.SUPERVISOR)
def edit_project(project_id):
project = Project.query.get_or_404(project_id)
if request.method == 'POST':
name = request.form.get('name')
description = request.form.get('description')
code = request.form.get('code')
team_id = request.form.get('team_id') or None
is_active = request.form.get('is_active') == 'on'
start_date_str = request.form.get('start_date')
end_date_str = request.form.get('end_date')
# Validate input
error = None
if not name:
error = 'Project name is required'
elif not code:
error = 'Project code is required'
elif code != project.code and Project.query.filter_by(code=code).first():
error = 'Project code already exists'
# Parse dates
start_date = None
end_date = None
if start_date_str:
try:
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
except ValueError:
error = 'Invalid start date format'
if end_date_str:
try:
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
except ValueError:
error = 'Invalid end date format'
if start_date and end_date and start_date > end_date:
error = 'Start date cannot be after end date'
if error is None:
project.name = name
project.description = description
project.code = code.upper()
project.team_id = int(team_id) if team_id else None
project.is_active = is_active
project.start_date = start_date
project.end_date = end_date
db.session.commit()
flash(f'Project "{name}" updated successfully!', 'success')
return redirect(url_for('admin_projects'))
else:
flash(error, 'error')
# Get available teams for the form
teams = Team.query.order_by(Team.name).all()
return render_template('edit_project.html', title='Edit Project', project=project, teams=teams)
@app.route('/admin/projects/delete/<int:project_id>', methods=['POST'])
@role_required(Role.ADMIN) # Only admins can delete projects
def delete_project(project_id):
project = Project.query.get_or_404(project_id)
# Check if there are time entries associated with this project
time_entries_count = TimeEntry.query.filter_by(project_id=project_id).count()
if time_entries_count > 0:
flash(f'Cannot delete project "{project.name}" - it has {time_entries_count} time entries associated with it. Deactivate the project instead.', 'error')
else:
project_name = project.name
db.session.delete(project)
db.session.commit()
flash(f'Project "{project_name}" deleted successfully!', 'success')
return redirect(url_for('admin_projects'))
@app.route('/api/team/hours_data', methods=['GET']) @app.route('/api/team/hours_data', methods=['GET'])
@login_required @login_required
@role_required(Role.TEAM_LEADER) # Only team leaders and above can access @role_required(Role.TEAM_LEADER) # Only team leaders and above can access
@@ -1345,12 +1547,15 @@ def prepare_export_data(entries):
for entry in entries: for entry in entries:
row = { row = {
'Date': entry.arrival_time.strftime('%Y-%m-%d'), 'Date': entry.arrival_time.strftime('%Y-%m-%d'),
'Project Code': entry.project.code if entry.project else '',
'Project Name': entry.project.name if entry.project else '',
'Arrival Time': entry.arrival_time.strftime('%H:%M:%S'), 'Arrival Time': entry.arrival_time.strftime('%H:%M:%S'),
'Departure Time': entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active', 'Departure Time': entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active',
'Work Duration (HH:MM:SS)': format_duration(entry.duration) if entry.duration is not None else 'In progress', 'Work Duration (HH:MM:SS)': format_duration(entry.duration) if entry.duration is not None else 'In progress',
'Break Duration (HH:MM:SS)': format_duration(entry.total_break_duration), 'Break Duration (HH:MM:SS)': format_duration(entry.total_break_duration),
'Work Duration (seconds)': entry.duration if entry.duration is not None else 0, 'Work Duration (seconds)': entry.duration if entry.duration is not None else 0,
'Break Duration (seconds)': entry.total_break_duration if entry.total_break_duration is not None else 0 'Break Duration (seconds)': entry.total_break_duration if entry.total_break_duration is not None else 0,
'Notes': entry.notes if entry.notes else ''
} }
data.append(row) data.append(row)
return data return data

View File

@@ -1,7 +1,7 @@
from app import app, db from app import app, db
import sqlite3 import sqlite3
import os import os
from models import User, TimeEntry, WorkConfig, SystemSettings, Team, Role from models import User, TimeEntry, WorkConfig, SystemSettings, Team, Role, Project
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from datetime import datetime from datetime import datetime
@@ -153,6 +153,40 @@ def migrate_database():
) )
""") """)
# Check if the project table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='project'")
if not cursor.fetchone():
print("Creating project table...")
cursor.execute("""
CREATE TABLE project (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
description TEXT,
code VARCHAR(20) NOT NULL UNIQUE,
is_active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by_id INTEGER NOT NULL,
team_id INTEGER,
start_date DATE,
end_date DATE,
FOREIGN KEY (created_by_id) REFERENCES user (id),
FOREIGN KEY (team_id) REFERENCES team (id)
)
""")
# Add project-related columns to time_entry table
cursor.execute("PRAGMA table_info(time_entry)")
time_entry_columns = [column[1] for column in cursor.fetchall()]
if 'project_id' not in time_entry_columns:
print("Adding project_id column to time_entry...")
cursor.execute("ALTER TABLE time_entry ADD COLUMN project_id INTEGER")
if 'notes' not in time_entry_columns:
print("Adding notes column to time_entry...")
cursor.execute("ALTER TABLE time_entry ADD COLUMN notes TEXT")
# Commit changes and close connection # Commit changes and close connection
conn.commit() conn.commit()
conn.close() conn.close()
@@ -230,6 +264,44 @@ def migrate_database():
print(f"Marked {len(existing_users)} existing users as verified") print(f"Marked {len(existing_users)} existing users as verified")
print(f"Updated {updated_count} users with default role and 2FA settings") print(f"Updated {updated_count} users with default role and 2FA settings")
# Create sample projects if none exist
existing_projects = Project.query.count()
if existing_projects == 0 and admin:
sample_projects = [
{
'name': 'General Administration',
'code': 'ADMIN001',
'description': 'General administrative tasks and meetings',
'team_id': None,
},
{
'name': 'Development Project',
'code': 'DEV001',
'description': 'Software development and maintenance tasks',
'team_id': None,
},
{
'name': 'Customer Support',
'code': 'SUPPORT001',
'description': 'Customer service and technical support activities',
'team_id': None,
}
]
for proj_data in sample_projects:
project = Project(
name=proj_data['name'],
code=proj_data['code'],
description=proj_data['description'],
team_id=proj_data['team_id'],
created_by_id=admin.id,
is_active=True
)
db.session.add(project)
db.session.commit()
print(f"Created {len(sample_projects)} sample projects")
def init_system_settings(): def init_system_settings():
"""Initialize system settings with default values if they don't exist""" """Initialize system settings with default values if they don't exist"""
# Check if registration_enabled setting exists # Check if registration_enabled setting exists

184
migrate_projects.py Normal file
View File

@@ -0,0 +1,184 @@
from app import app, db
from models import User, TimeEntry, Project, Team, Role
from sqlalchemy import text
import logging
from datetime import datetime
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def migrate_projects():
"""Migration script to add project time logging functionality"""
with app.app_context():
logger.info("Starting migration for project time logging...")
# Check if the project table exists
try:
# Create the project table if it doesn't exist
db.engine.execute(text("""
CREATE TABLE IF NOT EXISTS project (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
description TEXT,
code VARCHAR(20) NOT NULL UNIQUE,
is_active BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by_id INTEGER NOT NULL,
team_id INTEGER,
start_date DATE,
end_date DATE,
FOREIGN KEY (created_by_id) REFERENCES user (id),
FOREIGN KEY (team_id) REFERENCES team (id)
)
"""))
logger.info("Project table created or already exists")
except Exception as e:
logger.error(f"Error creating project table: {e}")
return
# Check if the time_entry table has the project-related columns
try:
# Check if project_id and notes columns exist in time_entry
result = db.engine.execute(text("PRAGMA table_info(time_entry)"))
columns = [row[1] for row in result]
if 'project_id' not in columns:
db.engine.execute(text("ALTER TABLE time_entry ADD COLUMN project_id INTEGER REFERENCES project(id)"))
logger.info("Added project_id column to time_entry table")
if 'notes' not in columns:
db.engine.execute(text("ALTER TABLE time_entry ADD COLUMN notes TEXT"))
logger.info("Added notes column to time_entry table")
except Exception as e:
logger.error(f"Error updating time_entry table: {e}")
return
# Create some default projects for demonstration
try:
# Check if any projects exist
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:
# Create some sample projects
sample_projects = [
{
'name': 'General Administration',
'code': 'ADMIN001',
'description': 'General administrative tasks and meetings',
'team_id': None, # Available to all teams
},
{
'name': 'Development Project',
'code': 'DEV001',
'description': 'Software development and maintenance tasks',
'team_id': None, # Available to all teams
},
{
'name': 'Customer Support',
'code': 'SUPPORT001',
'description': 'Customer service and technical support activities',
'team_id': None, # Available to all teams
}
]
for proj_data in sample_projects:
project = Project(
name=proj_data['name'],
code=proj_data['code'],
description=proj_data['description'],
team_id=proj_data['team_id'],
created_by_id=admin_user.id,
is_active=True
)
db.session.add(project)
db.session.commit()
logger.info(f"Created {len(sample_projects)} sample projects")
else:
logger.warning("No admin or supervisor user found to create sample projects")
else:
logger.info(f"Found {existing_projects} existing projects, skipping sample creation")
except Exception as e:
logger.error(f"Error creating sample projects: {e}")
db.session.rollback()
# Update database schema to match the current models
try:
db.create_all()
logger.info("Database schema updated successfully")
except Exception as e:
logger.error(f"Error updating database schema: {e}")
return
# Verify the migration
try:
# Check if we can query the new tables and columns
project_count = Project.query.count()
logger.info(f"Project table accessible with {project_count} projects")
# Check if time_entry has the new columns
result = db.engine.execute(text("PRAGMA table_info(time_entry)"))
columns = [row[1] for row in result]
required_columns = ['project_id', 'notes']
missing_columns = [col for col in required_columns if col not in columns]
if missing_columns:
logger.error(f"Missing columns in time_entry: {missing_columns}")
return
else:
logger.info("All required columns present in time_entry table")
except Exception as e:
logger.error(f"Error verifying migration: {e}")
return
logger.info("Project time logging migration completed successfully!")
print("\n" + "="*60)
print("PROJECT TIME LOGGING FEATURE ENABLED")
print("="*60)
print("✅ Project management interface available for Admins/Supervisors")
print("✅ Time tracking with optional project selection")
print("✅ Project-based reporting and filtering")
print("✅ Enhanced export functionality with project data")
print("\nAccess project management via:")
print("- Admin dropdown → Manage Projects")
print("- Supervisor dropdown → Manage Projects")
print("="*60)
def rollback_projects():
"""Rollback migration (removes project functionality)"""
with app.app_context():
logger.warning("Rolling back project time logging migration...")
try:
# Drop the project table
db.engine.execute(text("DROP TABLE IF EXISTS project"))
logger.info("Dropped project table")
# Note: SQLite doesn't support dropping columns, so we can't remove
# project_id and notes columns from time_entry table
logger.warning("Note: project_id and notes columns in time_entry table cannot be removed due to SQLite limitations")
logger.warning("These columns will remain but will not be used")
except Exception as e:
logger.error(f"Error during rollback: {e}")
return
logger.info("Project time logging rollback completed")
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "rollback":
rollback_projects()
else:
migrate_projects()

View File

@@ -26,6 +26,49 @@ class Team(db.Model):
def __repr__(self): def __repr__(self):
return f'<Team {self.name}>' return f'<Team {self.name}>'
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)
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)
# Foreign key to user who created the project (Admin/Supervisor)
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
# Optional team assignment - if set, only team members can log time to this project
team_id = db.Column(db.Integer, db.ForeignKey('team.id'), nullable=True)
# Project dates
start_date = db.Column(db.Date, nullable=True)
end_date = db.Column(db.Date, nullable=True)
# Relationships
created_by = db.relationship('User', foreign_keys=[created_by_id], backref='created_projects')
team = db.relationship('Team', backref='projects')
time_entries = db.relationship('TimeEntry', backref='project', lazy=True)
def __repr__(self):
return f'<Project {self.code}: {self.name}>'
def is_user_allowed(self, user):
"""Check if a user is allowed to log time to this project"""
if not self.is_active:
return False
# Admins and Supervisors can log time to any project
if user.role in [Role.ADMIN, Role.SUPERVISOR]:
return True
# If project is team-specific, only team members can log time
if self.team_id:
return user.team_id == self.team_id
# If no team restriction, any user can log time
return True
# Update User model to include role and team relationship # Update User model to include role and team relationship
class User(db.Model): class User(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@@ -127,8 +170,15 @@ class TimeEntry(db.Model):
total_break_duration = db.Column(db.Integer, default=0) # Total break duration in seconds total_break_duration = db.Column(db.Integer, default=0) # Total break duration in seconds
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
# Project association - nullable for backward compatibility
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=True)
# Optional notes/description for the time entry
notes = db.Column(db.Text, nullable=True)
def __repr__(self): def __repr__(self):
return f'<TimeEntry {self.id}: {self.arrival_time} - {self.departure_time}>' project_info = f" (Project: {self.project.code})" if self.project else ""
return f'<TimeEntry {self.id}: {self.arrival_time} - {self.departure_time}{project_info}>'
class WorkConfig(db.Model): class WorkConfig(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)

View File

@@ -58,20 +58,43 @@ document.addEventListener('DOMContentLoaded', function() {
// Handle arrive button click // Handle arrive button click
if (arriveBtn) { if (arriveBtn) {
arriveBtn.addEventListener('click', function() { arriveBtn.addEventListener('click', function() {
// Get project and notes from the form
const projectSelect = document.getElementById('project-select');
const notesTextarea = document.getElementById('work-notes');
const projectId = projectSelect ? projectSelect.value : null;
const notes = notesTextarea ? notesTextarea.value.trim() : null;
const requestData = {};
if (projectId) {
requestData.project_id = projectId;
}
if (notes) {
requestData.notes = notes;
}
fetch('/api/arrive', { fetch('/api/arrive', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} },
body: JSON.stringify(requestData)
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.error || 'Failed to record arrival time');
});
}
return response.json();
}) })
.then(response => response.json())
.then(data => { .then(data => {
// Reload the page to show the active timer // Reload the page to show the active timer
window.location.reload(); window.location.reload();
}) })
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
alert('Failed to record arrival time. Please try again.'); alert('Failed to record arrival time: ' + error.message);
}); });
}); });
} }

View File

@@ -0,0 +1,127 @@
{% extends "layout.html" %}
{% block content %}
<div class="timetrack-container">
<div class="admin-header">
<h2>Project Management</h2>
<a href="{{ url_for('create_project') }}" class="btn">Create New Project</a>
</div>
{% if projects %}
<div class="projects-table">
<table class="time-history">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Team</th>
<th>Status</th>
<th>Start Date</th>
<th>End Date</th>
<th>Created By</th>
<th>Time Entries</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for project in projects %}
<tr class="{% if not project.is_active %}inactive-project{% endif %}">
<td><strong>{{ project.code }}</strong></td>
<td>{{ project.name }}</td>
<td>
{% if project.team %}
{{ project.team.name }}
{% else %}
<em>All Teams</em>
{% endif %}
</td>
<td>
<span class="status-badge {% if project.is_active %}active{% else %}inactive{% endif %}">
{{ 'Active' if project.is_active else 'Inactive' }}
</span>
</td>
<td>{{ project.start_date.strftime('%Y-%m-%d') if project.start_date else '-' }}</td>
<td>{{ project.end_date.strftime('%Y-%m-%d') if project.end_date else '-' }}</td>
<td>{{ project.created_by.username }}</td>
<td>{{ project.time_entries|length }}</td>
<td class="actions">
<a href="{{ url_for('edit_project', project_id=project.id) }}" class="btn btn-small">Edit</a>
{% if g.user.role.name == 'ADMIN' and project.time_entries|length == 0 %}
<form method="POST" action="{{ url_for('delete_project', project_id=project.id) }}" style="display: inline;"
onsubmit="return confirm('Are you sure you want to delete this project?')">
<button type="submit" class="btn btn-small btn-danger">Delete</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="no-data">
<p>No projects found. <a href="{{ url_for('create_project') }}">Create your first project</a>.</p>
</div>
{% endif %}
</div>
<style>
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.projects-table {
overflow-x: auto;
}
.inactive-project {
opacity: 0.6;
}
.status-badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 500;
}
.status-badge.active {
background-color: #d4edda;
color: #155724;
}
.status-badge.inactive {
background-color: #f8d7da;
color: #721c24;
}
.actions {
white-space: nowrap;
}
.btn-small {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
margin-right: 0.5rem;
}
.btn-danger {
background-color: #dc3545;
border-color: #dc3545;
}
.btn-danger:hover {
background-color: #c82333;
border-color: #bd2130;
}
.no-data {
text-align: center;
padding: 3rem;
color: #666;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,136 @@
{% extends "layout.html" %}
{% block content %}
<div class="timetrack-container">
<h2>Create New Project</h2>
<form method="POST" action="{{ url_for('create_project') }}" class="project-form">
<div class="form-row">
<div class="form-group">
<label for="name">Project Name *</label>
<input type="text" id="name" name="name" required
value="{{ request.form.name if request.form.name else '' }}"
placeholder="Enter project name">
</div>
<div class="form-group">
<label for="code">Project Code *</label>
<input type="text" id="code" name="code" required
value="{{ request.form.code if request.form.code else '' }}"
placeholder="e.g., PRJ001" maxlength="20" style="text-transform: uppercase;">
</div>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" rows="3"
placeholder="Enter project description">{{ request.form.description if request.form.description else '' }}</textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="team_id">Assigned Team</label>
<select id="team_id" name="team_id">
<option value="">All Teams</option>
{% for team in teams %}
<option value="{{ team.id }}"
{% if request.form.team_id and request.form.team_id|int == team.id %}selected{% endif %}>
{{ team.name }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="start_date">Start Date</label>
<input type="date" id="start_date" name="start_date"
value="{{ request.form.start_date if request.form.start_date else '' }}">
</div>
<div class="form-group">
<label for="end_date">End Date</label>
<input type="date" id="end_date" name="end_date"
value="{{ request.form.end_date if request.form.end_date else '' }}">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn">Create Project</button>
<a href="{{ url_for('admin_projects') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
<style>
.project-form {
max-width: 600px;
margin: 0 auto;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s ease;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
}
.btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
}
.btn-secondary:hover {
background-color: #5a6268;
border-color: #545b62;
}
#code {
text-transform: uppercase;
}
</style>
<script>
// Auto-uppercase project code
document.getElementById('code').addEventListener('input', function(e) {
e.target.value = e.target.value.toUpperCase();
});
</script>
{% endblock %}

198
templates/edit_project.html Normal file
View File

@@ -0,0 +1,198 @@
{% extends "layout.html" %}
{% block content %}
<div class="timetrack-container">
<h2>Edit Project: {{ project.name }}</h2>
<form method="POST" action="{{ url_for('edit_project', project_id=project.id) }}" class="project-form">
<div class="form-row">
<div class="form-group">
<label for="name">Project Name *</label>
<input type="text" id="name" name="name" required
value="{{ request.form.name if request.form.name else project.name }}"
placeholder="Enter project name">
</div>
<div class="form-group">
<label for="code">Project Code *</label>
<input type="text" id="code" name="code" required
value="{{ request.form.code if request.form.code else project.code }}"
placeholder="e.g., PRJ001" maxlength="20" style="text-transform: uppercase;">
</div>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" rows="3"
placeholder="Enter project description">{{ request.form.description if request.form.description else (project.description or '') }}</textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="team_id">Assigned Team</label>
<select id="team_id" name="team_id">
<option value="">All Teams</option>
{% for team in teams %}
<option value="{{ team.id }}"
{% if (request.form.team_id and request.form.team_id|int == team.id) or (not request.form.team_id and project.team_id == team.id) %}selected{% endif %}>
{{ team.name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" name="is_active"
{% if (request.form.is_active and request.form.is_active == 'on') or (not request.form and project.is_active) %}checked{% endif %}>
Active Project
</label>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="start_date">Start Date</label>
<input type="date" id="start_date" name="start_date"
value="{{ request.form.start_date if request.form.start_date else (project.start_date.strftime('%Y-%m-%d') if project.start_date else '') }}">
</div>
<div class="form-group">
<label for="end_date">End Date</label>
<input type="date" id="end_date" name="end_date"
value="{{ request.form.end_date if request.form.end_date else (project.end_date.strftime('%Y-%m-%d') if project.end_date else '') }}">
</div>
</div>
<div class="project-info">
<div class="info-row">
<span class="info-label">Created by:</span>
<span class="info-value">{{ project.created_by.username }}</span>
</div>
<div class="info-row">
<span class="info-label">Created on:</span>
<span class="info-value">{{ project.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
</div>
<div class="info-row">
<span class="info-label">Time entries:</span>
<span class="info-value">{{ project.time_entries|length }}</span>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn">Update Project</button>
<a href="{{ url_for('admin_projects') }}" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
<style>
.project-form {
max-width: 600px;
margin: 0 auto;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
margin-top: 1.5rem;
}
.checkbox-label input[type="checkbox"] {
width: auto;
margin: 0;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s ease;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
}
.project-info {
background: #f8f9fa;
padding: 1rem;
border-radius: 6px;
margin: 1.5rem 0;
}
.info-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
font-weight: 500;
color: #666;
}
.info-value {
color: #333;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
}
.btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
}
.btn-secondary:hover {
background-color: #5a6268;
border-color: #545b62;
}
#code {
text-transform: uppercase;
}
</style>
<script>
// Auto-uppercase project code
document.getElementById('code').addEventListener('input', function(e) {
e.target.value = e.target.value.toUpperCase();
});
</script>
{% endblock %}

View File

@@ -7,16 +7,40 @@
<a href="{{ url_for('export') }}" class="btn">Export Data</a> <a href="{{ url_for('export') }}" class="btn">Export Data</a>
</div> </div>
<!-- Project Filter -->
<div class="filter-section">
<form method="GET" action="{{ url_for('history') }}" class="filter-form">
<div class="form-group">
<label for="project-filter">Filter by Project:</label>
<select id="project-filter" name="project_id" onchange="this.form.submit()">
<option value="">All Projects</option>
{% for project in available_projects %}
<option value="{{ project.id }}"
{% if request.args.get('project_id') and request.args.get('project_id')|int == project.id %}selected{% endif %}>
{{ project.code }} - {{ project.name }}
</option>
{% endfor %}
<option value="none"
{% if request.args.get('project_id') == 'none' %}selected{% endif %}>
No Project Assigned
</option>
</select>
</div>
</form>
</div>
<div class="history-section"> <div class="history-section">
{% if entries %} {% if entries %}
<table class="time-history"> <table class="time-history">
<thead> <thead>
<tr> <tr>
<th>Date</th> <th>Date</th>
<th>Project</th>
<th>Arrival</th> <th>Arrival</th>
<th>Departure</th> <th>Departure</th>
<th>Work Duration</th> <th>Work Duration</th>
<th>Break Duration</th> <th>Break Duration</th>
<th>Notes</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
@@ -24,10 +48,25 @@
{% for entry in entries %} {% for entry in entries %}
<tr data-entry-id="{{ entry.id }}"> <tr data-entry-id="{{ entry.id }}">
<td>{{ entry.arrival_time.strftime('%Y-%m-%d') }}</td> <td>{{ entry.arrival_time.strftime('%Y-%m-%d') }}</td>
<td>
{% if entry.project %}
<span class="project-tag">{{ entry.project.code }}</span>
<small>{{ entry.project.name }}</small>
{% else %}
<em>No project</em>
{% endif %}
</td>
<td>{{ entry.arrival_time.strftime('%H:%M:%S') }}</td> <td>{{ entry.arrival_time.strftime('%H:%M:%S') }}</td>
<td>{{ entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active' }}</td> <td>{{ entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active' }}</td>
<td>{{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) if entry.duration is not none else 'In progress' }}</td> <td>{{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) if entry.duration is not none else 'In progress' }}</td>
<td>{{ '%d:%02d:%02d'|format(entry.total_break_duration//3600, (entry.total_break_duration%3600)//60, entry.total_break_duration%60) if entry.total_break_duration is not none else '00:00:00' }}</td> <td>{{ '%d:%02d:%02d'|format(entry.total_break_duration//3600, (entry.total_break_duration%3600)//60, entry.total_break_duration%60) if entry.total_break_duration is not none else '00:00:00' }}</td>
<td>
{% if entry.notes %}
<span class="notes-preview" title="{{ entry.notes }}">{{ entry.notes[:50] }}{% if entry.notes|length > 50 %}...{% endif %}</span>
{% else %}
<em>-</em>
{% endif %}
</td>
<td> <td>
<button class="edit-entry-btn" data-id="{{ entry.id }}">Edit</button> <button class="edit-entry-btn" data-id="{{ entry.id }}">Edit</button>
<button class="delete-entry-btn" data-id="{{ entry.id }}">Delete</button> <button class="delete-entry-btn" data-id="{{ entry.id }}">Delete</button>
@@ -234,4 +273,70 @@
}); });
}); });
</script> </script>
<style>
.filter-section {
background: #f8f9fa;
padding: 1rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.filter-form .form-group {
margin: 0;
}
.filter-form label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.filter-form select {
width: 100%;
max-width: 300px;
padding: 0.5rem;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s ease;
}
.filter-form select:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
}
.project-tag {
background: #4CAF50;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
margin-right: 0.5rem;
}
.project-tag + small {
color: #666;
font-size: 0.85rem;
}
.notes-preview {
color: #666;
font-size: 0.9rem;
cursor: help;
}
.time-history td {
vertical-align: middle;
}
.time-history .project-tag + small {
display: block;
margin-top: 0.25rem;
}
</style>
{% endblock %} {% endblock %}

View File

@@ -20,6 +20,9 @@ Please <a href="{{ url_for('login') }}">login</a> or <a href="{{ url_for('regist
<div class="active-timer"> <div class="active-timer">
<h3>Currently Working</h3> <h3>Currently Working</h3>
<p>Started at: {{ active_entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S') }}</p> <p>Started at: {{ active_entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S') }}</p>
{% if active_entry.project %}
<p class="project-info">Project: <strong>{{ active_entry.project.code }} - {{ active_entry.project.name }}</strong></p>
{% endif %}
<div id="timer" <div id="timer"
data-start="{{ active_entry.arrival_time.timestamp() }}" data-start="{{ active_entry.arrival_time.timestamp() }}"
data-breaks="{{ active_entry.total_break_duration }}" data-breaks="{{ active_entry.total_break_duration }}"
@@ -43,8 +46,23 @@ Please <a href="{{ url_for('login') }}">login</a> or <a href="{{ url_for('regist
{% else %} {% else %}
<div class="inactive-timer"> <div class="inactive-timer">
<h3>Not Currently Working</h3> <h3>Not Currently Working</h3>
<div class="start-work-form">
<div class="form-group">
<label for="project-select">Select Project (Optional):</label>
<select id="project-select" name="project_id">
<option value="">No specific project</option>
{% for project in available_projects %}
<option value="{{ project.id }}">{{ project.code }} - {{ project.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="work-notes">Notes (Optional):</label>
<textarea id="work-notes" name="notes" rows="2" placeholder="What are you working on?"></textarea>
</div>
<button id="arrive-btn" class="arrive-btn">Arrive</button> <button id="arrive-btn" class="arrive-btn">Arrive</button>
</div> </div>
</div>
{% endif %} {% endif %}
</div> </div>
@@ -55,6 +73,7 @@ Please <a href="{{ url_for('login') }}">login</a> or <a href="{{ url_for('regist
<thead> <thead>
<tr> <tr>
<th>Date</th> <th>Date</th>
<th>Project</th>
<th>Arrival</th> <th>Arrival</th>
<th>Departure</th> <th>Departure</th>
<th>Work Duration</th> <th>Work Duration</th>
@@ -66,6 +85,14 @@ Please <a href="{{ url_for('login') }}">login</a> or <a href="{{ url_for('regist
{% for entry in history %} {% for entry in history %}
<tr data-entry-id="{{ entry.id }}"> <tr data-entry-id="{{ entry.id }}">
<td>{{ entry.arrival_time.strftime('%Y-%m-%d') }}</td> <td>{{ entry.arrival_time.strftime('%Y-%m-%d') }}</td>
<td>
{% if entry.project %}
<span class="project-tag">{{ entry.project.code }}</span>
<small>{{ entry.project.name }}</small>
{% else %}
<em>No project</em>
{% endif %}
</td>
<td>{{ entry.arrival_time.strftime('%H:%M:%S') }}</td> <td>{{ entry.arrival_time.strftime('%H:%M:%S') }}</td>
<td>{{ entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active' }}</td> <td>{{ entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active' }}</td>
<td>{{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) if entry.duration is not none else 'In progress' }}</td> <td>{{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) if entry.duration is not none else 'In progress' }}</td>
@@ -310,4 +337,71 @@ Please <a href="{{ url_for('login') }}">login</a> or <a href="{{ url_for('regist
}); });
</script> </script>
<style>
.start-work-form {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.start-work-form .form-group {
margin-bottom: 1rem;
}
.start-work-form label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.start-work-form select,
.start-work-form textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s ease;
}
.start-work-form select:focus,
.start-work-form textarea:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
}
.project-info {
color: #4CAF50;
font-size: 0.9rem;
margin-top: 0.5rem;
}
.project-tag {
background: #4CAF50;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
margin-right: 0.5rem;
}
.project-tag + small {
color: #666;
font-size: 0.85rem;
}
.time-history td {
vertical-align: middle;
}
.time-history .project-tag + small {
display: block;
margin-top: 0.25rem;
}
</style>
{% endblock %} {% endblock %}

View File

@@ -23,6 +23,14 @@
<li><a href="{{ url_for('profile') }}">Profile</a></li> <li><a href="{{ url_for('profile') }}">Profile</a></li>
<li><a href="{{ url_for('config') }}">Config</a></li> <li><a href="{{ url_for('config') }}">Config</a></li>
<li><a href="{{ url_for('dashboard') }}">Dashboard</a></li> <li><a href="{{ url_for('dashboard') }}">Dashboard</a></li>
<li><a href="{{ url_for('admin_dashboard') }}">Admin Dashboard</a></li>
<li><a href="{{ url_for('admin_users') }}">Manage Users</a></li>
<li><a href="{{ url_for('admin_teams') }}">Manage Teams</a></li>
<li><a href="{{ url_for('admin_projects') }}">Manage Projects</a></li>
<li><a href="{{ url_for('admin_settings') }}">System Settings</a></li>
{% if g.user.team_id %}
<li><a href="{{ url_for('team_hours') }}">Team Hours</a></li>
{% endif %}
<li><a href="{{ url_for('logout') }}">Logout</a></li> <li><a href="{{ url_for('logout') }}">Logout</a></li>
</ul> </ul>
</li> </li>
@@ -34,6 +42,9 @@
<li><a href="{{ url_for('profile') }}">Profile</a></li> <li><a href="{{ url_for('profile') }}">Profile</a></li>
<li><a href="{{ url_for('config') }}">Config</a></li> <li><a href="{{ url_for('config') }}">Config</a></li>
<li><a href="{{ url_for('dashboard') }}">Dashboard</a></li> <li><a href="{{ url_for('dashboard') }}">Dashboard</a></li>
{% if g.user.role == Role.SUPERVISOR %}
<li><a href="{{ url_for('admin_projects') }}">Manage Projects</a></li>
{% endif %}
{% if g.user.team_id %} {% if g.user.team_id %}
<li><a href="{{ url_for('team_hours') }}">Team Hours</a></li> <li><a href="{{ url_for('team_hours') }}">Team Hours</a></li>
{% endif %} {% endif %}