diff --git a/MIGRATION_PROJECTS.md b/MIGRATION_PROJECTS.md new file mode 100644 index 0000000..39a25a3 --- /dev/null +++ b/MIGRATION_PROJECTS.md @@ -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. \ No newline at end of file diff --git a/app.py b/app.py index 0cc947d..ef12693 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,5 @@ 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 from datetime import datetime, time, timedelta import os @@ -154,7 +154,17 @@ def home(): TimeEntry.arrival_time <= datetime.combine(today, time.max) ).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: return render_template('index.html', title='Home') @@ -683,14 +693,35 @@ def timetrack(): @app.route('/api/arrive', methods=['POST']) @login_required 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 - 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.commit() return jsonify({ '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/', methods=['POST']) @@ -900,10 +931,38 @@ def team_hours(): @app.route('/history') @login_required def history(): - # Get all time entries for the current user, ordered by most recent first - all_entries = TimeEntry.query.filter_by(user_id=session['user_id']).order_by(TimeEntry.arrival_time.desc()).all() + # Get project filter from query parameters + project_filter = request.args.get('project_id') + + # 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) + 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): """ @@ -1187,6 +1246,149 @@ def manage_team(team_id): 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/', 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/', 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']) @login_required @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: row = { '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'), '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', 'Break Duration (HH:MM:SS)': format_duration(entry.total_break_duration), '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) return data diff --git a/migrate_db.py b/migrate_db.py index 703f85a..af61e0b 100644 --- a/migrate_db.py +++ b/migrate_db.py @@ -1,7 +1,7 @@ from app import app, db import sqlite3 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 datetime import datetime @@ -152,6 +152,40 @@ def migrate_database(): updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) + + # 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 conn.commit() @@ -229,6 +263,44 @@ def migrate_database(): print(f"Associated {len(orphan_configs)} existing work configs with admin user") print(f"Marked {len(existing_users)} existing users as verified") 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(): """Initialize system settings with default values if they don't exist""" diff --git a/migrate_projects.py b/migrate_projects.py new file mode 100644 index 0000000..ebdf956 --- /dev/null +++ b/migrate_projects.py @@ -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() \ No newline at end of file diff --git a/models.py b/models.py index bdebfda..6a1c87e 100644 --- a/models.py +++ b/models.py @@ -26,6 +26,49 @@ class Team(db.Model): def __repr__(self): return f'' +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'' + + 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 class User(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -126,9 +169,16 @@ class TimeEntry(db.Model): pause_start_time = db.Column(db.DateTime, nullable=True) 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) + + # 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): - return f'' + project_info = f" (Project: {self.project.code})" if self.project else "" + return f'' class WorkConfig(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/static/js/script.js b/static/js/script.js index 51ba52f..857c985 100644 --- a/static/js/script.js +++ b/static/js/script.js @@ -58,20 +58,43 @@ document.addEventListener('DOMContentLoaded', function() { // Handle arrive button click if (arriveBtn) { 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', { method: 'POST', headers: { '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 => { // Reload the page to show the active timer window.location.reload(); }) .catch(error => { console.error('Error:', error); - alert('Failed to record arrival time. Please try again.'); + alert('Failed to record arrival time: ' + error.message); }); }); } diff --git a/templates/admin_projects.html b/templates/admin_projects.html new file mode 100644 index 0000000..b616396 --- /dev/null +++ b/templates/admin_projects.html @@ -0,0 +1,127 @@ +{% extends "layout.html" %} + +{% block content %} +
+
+

Project Management

+ Create New Project +
+ + {% if projects %} +
+ + + + + + + + + + + + + + + + {% for project in projects %} + + + + + + + + + + + + {% endfor %} + +
CodeNameTeamStatusStart DateEnd DateCreated ByTime EntriesActions
{{ project.code }}{{ project.name }} + {% if project.team %} + {{ project.team.name }} + {% else %} + All Teams + {% endif %} + + + {{ 'Active' if project.is_active else 'Inactive' }} + + {{ project.start_date.strftime('%Y-%m-%d') if project.start_date else '-' }}{{ project.end_date.strftime('%Y-%m-%d') if project.end_date else '-' }}{{ project.created_by.username }}{{ project.time_entries|length }} + Edit + {% if g.user.role.name == 'ADMIN' and project.time_entries|length == 0 %} +
+ +
+ {% endif %} +
+
+ {% else %} +
+

No projects found. Create your first project.

+
+ {% endif %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/create_project.html b/templates/create_project.html new file mode 100644 index 0000000..c9d4f12 --- /dev/null +++ b/templates/create_project.html @@ -0,0 +1,136 @@ +{% extends "layout.html" %} + +{% block content %} +
+

Create New Project

+ +
+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + Cancel +
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/edit_project.html b/templates/edit_project.html new file mode 100644 index 0000000..58636a3 --- /dev/null +++ b/templates/edit_project.html @@ -0,0 +1,198 @@ +{% extends "layout.html" %} + +{% block content %} +
+

Edit Project: {{ project.name }}

+ +
+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+
+ + +
+ +
+ +
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ Created by: + {{ project.created_by.username }} +
+
+ Created on: + {{ project.created_at.strftime('%Y-%m-%d %H:%M') }} +
+
+ Time entries: + {{ project.time_entries|length }} +
+
+ +
+ + Cancel +
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/history.html b/templates/history.html index b80ce2e..3633e4a 100644 --- a/templates/history.html +++ b/templates/history.html @@ -7,16 +7,40 @@ Export Data + +
+
+
+ + +
+
+
+
{% if entries %} + + @@ -24,10 +48,25 @@ {% for entry in entries %} + + + @@ -310,4 +337,71 @@ Please login or Profile
  • Config
  • Dashboard
  • +
  • Admin Dashboard
  • +
  • Manage Users
  • +
  • Manage Teams
  • +
  • Manage Projects
  • +
  • System Settings
  • + {% if g.user.team_id %} +
  • Team Hours
  • + {% endif %}
  • Logout
  • @@ -34,6 +42,9 @@
  • Profile
  • Config
  • Dashboard
  • + {% if g.user.role == Role.SUPERVISOR %} +
  • Manage Projects
  • + {% endif %} {% if g.user.team_id %}
  • Team Hours
  • {% endif %}
    DateProject Arrival Departure Work Duration Break DurationNotes Actions
    {{ entry.arrival_time.strftime('%Y-%m-%d') }} + {% if entry.project %} + {{ entry.project.code }} + {{ entry.project.name }} + {% else %} + No project + {% endif %} + {{ entry.arrival_time.strftime('%H:%M:%S') }} {{ entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active' }} {{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) if entry.duration is not none else 'In progress' }} {{ '%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' }} + {% if entry.notes %} + {{ entry.notes[:50] }}{% if entry.notes|length > 50 %}...{% endif %} + {% else %} + - + {% endif %} + @@ -234,4 +273,70 @@ }); }); + + {% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 372a5e9..457ae41 100644 --- a/templates/index.html +++ b/templates/index.html @@ -20,6 +20,9 @@ Please login or

    Currently Working

    Started at: {{ active_entry.arrival_time.strftime('%Y-%m-%d %H:%M:%S') }}

    + {% if active_entry.project %} +

    Project: {{ active_entry.project.code }} - {{ active_entry.project.name }}

    + {% endif %}
    {% endif %} @@ -55,6 +73,7 @@ Please login or login or
    {{ entry.arrival_time.strftime('%Y-%m-%d') }} + {% if entry.project %} + {{ entry.project.code }} + {{ entry.project.name }} + {% else %} + No project + {% endif %} + {{ entry.arrival_time.strftime('%H:%M:%S') }} {{ entry.departure_time.strftime('%H:%M:%S') if entry.departure_time else 'Active' }} {{ '%d:%02d:%02d'|format(entry.duration//3600, (entry.duration%3600)//60, entry.duration%60) if entry.duration is not none else 'In progress' }}