feat: Enhance task management with modern UI/UX design and responsive layout

 Major UI/UX Improvements:
- Redesign task management interface with modern card-based layout
- Implement responsive design optimized for all devices
- Add hover effects, smooth transitions, and modern animations
- Integrate Bootstrap 5 with custom CSS variables and styling

🎨 Enhanced Task Templates:
- tasks/list.html: Modern header, quick stats, advanced filtering, card grid
- tasks/view.html: Comprehensive task overview with timeline and quick actions
- tasks/create.html: Enhanced form with helpful sidebar and validation
- tasks/edit.html: Improved editing interface with current task context
- tasks/my_tasks.html: Personalized task view with task type indicators

🔧 Technical Improvements:
- Fix CSRF token errors by removing Flask-WTF dependencies
- Convert templates to use regular HTML forms matching route implementation
- Ensure proper form validation and user experience
- Maintain all existing functionality while improving interface

📱 Mobile-First Design:
- Responsive grid layouts that stack properly on mobile
- Touch-friendly buttons and interactions
- Optimized spacing and typography for all screen sizes
- Consistent design system across all task views

📊 Enhanced Features:
- Quick stats overview showing task distribution by status
- Advanced filtering with search, status, priority, project, and assignee
- Priority-based color coding and visual indicators
- Task timeline visualization for better project tracking
- Improved form layouts with icons and helpful guidance

📚 Documentation Updates:
- Update README.md with comprehensive task management feature descriptions
- Add new screenshot section for enhanced task interface
- Document modern UI/UX improvements and technical features
- Include usage examples and workflow descriptions

�� User Experience:
- Clean, professional appearance suitable for business use
- Intuitive navigation and clear visual hierarchy
- Consistent styling with existing application design
- Improved accessibility and usability across all devices

This commit represents a significant enhancement to the task management system,
transforming it from a basic interface to a modern, professional-grade
solution that matches contemporary web application standards.
This commit is contained in:
Dries Peeters
2025-08-29 14:14:08 +02:00
parent 98728691ef
commit 1865a5a1b8
19 changed files with 3045 additions and 867 deletions

View File

@@ -28,21 +28,11 @@ COPY . .
# Create data and logs directories with proper permissions
RUN mkdir -p /data /app/logs && chmod 755 /data && chmod 755 /app/logs
# Create startup script directly in Dockerfile
RUN echo '#!/bin/bash' > /app/start.sh && \
echo 'set -e' >> /app/start.sh && \
echo 'cd /app' >> /app/start.sh && \
echo 'export FLASK_APP=app' >> /app/start.sh && \
echo 'echo "=== Starting TimeTracker ==="' >> /app/start.sh && \
echo 'echo "Testing startup script..."' >> /app/start.sh && \
echo 'ls -la /app/docker/' >> /app/start.sh && \
echo 'echo "Starting database initialization..."' >> /app/start.sh && \
echo 'python /app/docker/init-database-sql.py' >> /app/start.sh && \
echo 'echo "Starting application..."' >> /app/start.sh && \
echo 'exec gunicorn --bind 0.0.0.0:8080 --worker-class eventlet --workers 1 --timeout 120 "app:create_app()"' >> /app/start.sh
# Copy the fixed startup script
COPY docker/start-fixed.sh /app/start.sh
# Make startup scripts executable
RUN chmod +x /app/start.sh /app/docker/init-database.py /app/docker/init-database-sql.py /app/docker/test-db.py
RUN chmod +x /app/start.sh /app/docker/init-database.py /app/docker/init-database-sql.py /app/docker/test-db.py /app/docker/start-fixed.sh
# Create non-root user
RUN useradd -m -u 1000 timetracker && \

View File

@@ -6,7 +6,7 @@
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Platform](https://img.shields.io/badge/Platform-Raspberry%20Pi-red.svg)](https://www.raspberrypi.org/)
A robust, self-hosted time tracking application designed for teams and freelancers who need reliable time management without cloud dependencies. Built with Flask and optimized for Raspberry Pi deployment, TimeTracker provides persistent timers, comprehensive reporting, and a modern web interface.
A robust, self-hosted time tracking application designed for teams and freelancers who need reliable time management without cloud dependencies. Built with Flask and optimized for Raspberry Pi deployment, TimeTracker provides persistent timers, comprehensive reporting, and a modern web interface with enhanced task management capabilities.
## 🎯 What Problem Does It Solve?
@@ -17,12 +17,14 @@ A robust, self-hosted time tracking application designed for teams and freelance
- **Complex Setup**: Simple Docker deployment on Raspberry Pi or any Linux system
- **Limited Reporting**: Built-in comprehensive reports and CSV exports
- **Team Management**: User roles, project organization, and billing support
- **Task Organization**: Break down projects into manageable tasks with modern UI/UX
**Perfect for:**
- Freelancers tracking billable hours
- Small teams managing project time
- Consultants needing client billing reports
- Anyone wanting self-hosted time tracking
- Project managers organizing work into tasks
- Anyone wanting self-hosted time tracking with task management
## ✨ Features
@@ -51,22 +53,30 @@ A robust, self-hosted time tracking application designed for teams and freelance
- **Project Status**: Active, completed, and archived projects
- **Time Rounding**: Configurable time rounding for billing
### ✅ Task Management
- **Project Breakdown**: Break projects into manageable tasks
- **Status Tracking**: Monitor task progress (To Do, In Progress, Review, Done)
- **Priority Management**: Set and track task priorities (Low, Medium, High, Urgent)
- **Time Estimation**: Estimate and track actual time for tasks
- **Task Assignment**: Assign tasks to team members
- **Due Date Tracking**: Set deadlines with overdue notifications
### ✅ Enhanced Task Management
- **Modern UI/UX Design**: Beautiful, responsive card-based layout with hover effects
- **Project Breakdown**: Break projects into manageable tasks with visual hierarchy
- **Status Tracking**: Monitor task progress (To Do, In Progress, Review, Done, Cancelled)
- **Priority Management**: Set and track task priorities (Low, Medium, High, Urgent) with color-coded badges
- **Time Estimation**: Estimate and track actual time for tasks with progress bars
- **Task Assignment**: Assign tasks to team members with user avatars
- **Due Date Tracking**: Set deadlines with overdue notifications and visual indicators
- **Quick Actions**: One-click status updates and task management
- **Advanced Filtering**: Search, filter by status/priority/project/assignee
- **Task Timeline**: Visual timeline showing task creation, assignment, and completion events
- **Mobile-First Design**: Responsive design that works perfectly on all devices
- **Interactive Elements**: Hover effects, smooth transitions, and modern animations
- **Quick Stats**: Overview cards showing task counts by status
- **Automatic Migration**: Database tables are automatically created on first startup
### 🚀 Technical Features
- **Responsive Design**: Works on desktop, tablet, and mobile
- **Responsive Design**: Works on desktop, tablet, and mobile with modern UI components
- **HTMX Integration**: Dynamic interactions without JavaScript complexity
- **PostgreSQL Database**: Robust database with automatic initialization
- **Docker Ready**: Easy deployment and scaling
- **RESTful API**: Programmatic access to time data
- **Timezone Management**: Comprehensive timezone support with 100+ options
- **Modern CSS**: Bootstrap 5 with custom styling and CSS variables
## 🖼️ Screenshots
@@ -76,6 +86,14 @@ A robust, self-hosted time tracking application designed for teams and freelance
- Quick access to start/stop timers and manual time entry
- Real-time timer status and project selection
### Enhanced Task Management
![Task Management](assets/screenshots/Task_Management.png)
- Modern card-based layout with priority-based color coding
- Quick stats overview showing task distribution by status
- Advanced filtering and search capabilities
- Responsive design optimized for all devices
- Interactive elements with hover effects and smooth transitions
### Project Management
![Projects](assets/screenshots/Projects.png)
- Client and project organization with billing information
@@ -123,6 +141,7 @@ The **simple container** is an all-in-one solution that includes both the TimeTr
-**Persistent storage**: Data survives container restarts
-**Production ready**: Optimized for deployment
-**Timezone support**: Full timezone management with 100+ options
-**Enhanced UI**: Modern task management interface with responsive design
**Run with docker-compose:**
```bash
@@ -225,6 +244,7 @@ docker-compose -f docker-compose.simple.yml up -d
- ✅ **Persistent storage** - Data survives restarts
- ✅ **Simple setup** - One command deployment
- ✅ **Timezone support** - 100+ timezone options with automatic DST handling
- ✅ **Enhanced task management** - Modern UI with responsive design
**Default credentials:**
- **Username**: `admin`
@@ -234,7 +254,7 @@ docker-compose -f docker-compose.simple.yml up -d
The container automatically:
1. Creates a PostgreSQL database named `timetracker`
2. Creates a user `timetracker` with full permissions
3. Initializes all tables with proper schema
3. Initializes all tables with proper schema including task management
4. Inserts default admin user and project
5. Sets up triggers for automatic timestamp updates
@@ -327,6 +347,16 @@ The container automatically:
4. **Add notes** to describe what you're working on
5. **Timer runs continuously** even if you close the browser
### Managing Tasks
1. **Navigate to "Tasks"** in the main menu
2. **Create new tasks** with the enhanced form interface
3. **Set priorities and due dates** with visual indicators
4. **Assign tasks** to team members
5. **Track progress** with status updates and time logging
6. **Use advanced filtering** to find specific tasks
7. **View task details** with comprehensive information and timeline
### Manual Time Entry
1. **Go to "Manual Entry"** in the main menu
@@ -362,10 +392,11 @@ The container automatically:
- **Backend**: Flask with SQLAlchemy ORM
- **Database**: PostgreSQL with automatic initialization
- **Frontend**: Server-rendered templates with HTMX
- **Frontend**: Server-rendered templates with modern CSS and responsive design
- **Real-time**: WebSocket for live timer updates
- **Containerization**: Docker with docker-compose
- **Timezone**: Full timezone support with pytz
- **UI Framework**: Bootstrap 5 with custom CSS variables and animations
### Project Structure
@@ -374,7 +405,7 @@ TimeTracker/
├── app/ # Flask application
│ ├── models/ # Database models
│ ├── routes/ # Route handlers
│ ├── templates/ # Jinja2 templates
│ ├── templates/ # Jinja2 templates with enhanced task management
│ ├── utils/ # Utility functions
│ └── config.py # Configuration settings
├── docker/ # Docker configuration
@@ -393,6 +424,7 @@ TimeTracker/
- **Users**: Username-based authentication with role-based access
- **Projects**: Client projects with billing information and client management
- **Time Entries**: Manual and automatic time tracking with notes, tags, and billing support
- **Tasks**: Project breakdown with status, priority, assignment, and time tracking
- **Settings**: System configuration including timezone preferences
#### Database Schema
@@ -406,7 +438,10 @@ The simple container automatically creates and initializes a PostgreSQL database
- `id`, `name`, `client`, `description`, `billable`, `hourly_rate`, `billing_ref`, `status`, `created_at`, `updated_at`
**Time Entries Table:**
- `id`, `user_id`, `project_id`, `start_utc`, `end_utc`, `duration_seconds`, `notes`, `tags`, `source`, `billable`, `created_at`, `updated_at`
- `id`, `user_id`, `project_id`, `task_id`, `start_utc`, `end_utc`, `duration_seconds`, `notes`, `tags`, `source`, `billable`, `created_at`, `updated_at`
**Tasks Table:**
- `id`, `project_id`, `name`, `description`, `status`, `priority`, `assigned_to`, `created_by`, `due_date`, `estimated_hours`, `actual_hours`, `started_at`, `completed_at`, `created_at`, `updated_at`
**Settings Table:**
- `id`, `timezone`, `currency`, `rounding_minutes`, `single_active_timer`, `allow_self_register`, `idle_timeout_minutes`, `backup_retention_days`, `backup_time`, `export_delimiter`, `created_at`, `updated_at`
@@ -416,8 +451,9 @@ The simple container automatically creates and initializes a PostgreSQL database
- **Timer Persistence**: Active timers survive server restarts
- **Billing Support**: Hourly rates, billable flags, and cost calculations
- **Export Capabilities**: CSV export for reports and data backup
- **Responsive Design**: Works on desktop and mobile devices
- **Responsive Design**: Works on desktop and mobile devices with modern UI
- **Timezone Support**: Full timezone awareness with automatic DST handling
- **Task Management**: Comprehensive task organization with modern interface
## 🛠️ Development
@@ -468,6 +504,7 @@ python -m pytest tests/test_timer.py
- **Type Hints**: Python type annotations where appropriate
- **Documentation**: Docstrings for all public functions
- **Testing**: Comprehensive test coverage
- **UI/UX**: Modern design principles with accessibility considerations
## 🔒 Security Considerations
@@ -526,6 +563,7 @@ The GPL v3 license ensures that:
- **Docker issues**: Verify Docker and Docker Compose installation
- **Network access**: Check firewall settings and port configuration
- **Timezone issues**: Verify timezone settings in admin panel
- **Task management**: Ensure database schema is properly initialized
## 🚀 Roadmap
@@ -537,6 +575,8 @@ The GPL v3 license ensures that:
- [ ] **Team Collaboration**: Shared projects and time approval workflows
- [ ] **Integration**: Zapier, Slack, and other platform connections
- [ ] **Multi-language**: Internationalization support
- [ ] **Task Templates**: Predefined task structures for common workflows
- [ ] **Advanced Notifications**: Email and push notifications for task updates
### Recent Updates
@@ -544,12 +584,14 @@ The GPL v3 license ensures that:
- **v1.1.0**: Added comprehensive reporting and export capabilities
- **v1.2.0**: Enhanced project management and billing support
- **v1.3.0**: Added comprehensive timezone support with 100+ options
- **v1.4.0**: Enhanced task management with modern UI/UX design and responsive layout
## 🙏 Acknowledgments
- **Flask Community**: For the excellent web framework
- **SQLAlchemy Team**: For robust database ORM
- **Docker Community**: For containerization tools
- **Bootstrap Team**: For the responsive CSS framework
- **Contributors**: Everyone who has helped improve TimeTracker
---

View File

@@ -4,79 +4,124 @@
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<div class="card mobile-card">
<div class="card-body py-4">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 48px; height: 48px;">
<i class="fas fa-plus text-primary fa-lg"></i>
</div>
<div>
<h1 class="h2 mb-1">Create New Task</h1>
<p class="text-muted mb-0">Add a new task to your project to break down work into manageable components</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Create Task Form -->
<div class="row">
<div class="col-lg-8">
<div class="card mobile-card">
<div class="card-header">
<h2 class="mb-0">
<i class="fas fa-plus"></i> Create New Task
</h2>
<h6 class="mb-0">
<i class="fas fa-edit me-2 text-primary"></i>Task Information
</h6>
</div>
<div class="card-body">
<form method="POST">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="name" class="form-label">Task Name *</label>
<input type="text" class="form-control" id="name" name="name"
value="{{ request.form.get('name', '') }}" required>
<div class="form-text">Give your task a clear, descriptive name</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="priority" class="form-label">Priority</label>
<select class="form-select" id="priority" name="priority">
<option value="low" {% if request.form.get('priority') == 'low' %}selected{% endif %}>Low</option>
<option value="medium" {% if request.form.get('priority') == 'medium' or not request.form.get('priority') %}selected{% endif %}>Medium</option>
<option value="high" {% if request.form.get('priority') == 'high' %}selected{% endif %}>High</option>
<option value="urgent" {% if request.form.get('priority') == 'urgent' %}selected{% endif %}>Urgent</option>
</select>
</div>
</div>
<form method="POST" id="createTaskForm">
<!-- Task Name -->
<div class="mb-4">
<label for="name" class="form-label fw-semibold">
<i class="fas fa-tag me-2 text-primary"></i>Task Name <span class="text-danger">*</span>
</label>
<input type="text" class="form-control form-control-lg" id="name" name="name"
value="{{ request.form.get('name', '') }}" placeholder="Enter a descriptive task name" required>
<small class="form-text text-muted">Choose a clear, descriptive name that explains what needs to be done</small>
</div>
<div class="mb-3">
<label for="project_id" class="form-label">Project *</label>
<select class="form-select" id="project_id" name="project_id" required>
<!-- Description -->
<div class="mb-4">
<label for="description" class="form-label fw-semibold">
<i class="fas fa-align-left me-2 text-primary"></i>Description
</label>
<textarea class="form-control" id="description" name="description" rows="4"
placeholder="Provide detailed information about the task, requirements, and any specific instructions...">{{ request.form.get('description', '') }}</textarea>
<small class="form-text text-muted">Optional: Add context, requirements, or specific instructions for the task</small>
</div>
<!-- Project Selection -->
<div class="mb-4">
<label for="project_id" class="form-label fw-semibold">
<i class="fas fa-project-diagram me-2 text-primary"></i>Project <span class="text-danger">*</span>
</label>
<select class="form-select form-select-lg" id="project_id" name="project_id" required>
<option value="">Select a project</option>
{% for project in projects %}
<option value="{{ project.id }}" {% if request.form.get('project_id')|int == project.id or request.args.get('project_id')|int == project.id %}selected{% endif %}>
{{ project.name }} ({{ project.client }})
{{ project.name }}
</option>
{% endfor %}
</select>
<small class="form-text text-muted">Select the project this task belongs to</small>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="4"
placeholder="Describe what needs to be done...">{{ request.form.get('description', '') }}</textarea>
<div class="form-text">Provide details about the task requirements and deliverables</div>
</div>
<div class="row">
<!-- Priority and Status -->
<div class="row mb-4">
<div class="col-md-6">
<div class="mb-3">
<label for="estimated_hours" class="form-label">Estimated Hours</label>
<input type="number" class="form-control" id="estimated_hours" name="estimated_hours"
value="{{ request.form.get('estimated_hours', '') }}"
step="0.5" min="0" placeholder="e.g., 8.5">
<div class="form-text">How long do you think this task will take?</div>
</div>
<label for="priority" class="form-label fw-semibold">
<i class="fas fa-flag me-2 text-warning"></i>Priority
</label>
<select class="form-select" id="priority" name="priority">
<option value="low" {% if request.form.get('priority') == 'low' %}selected{% endif %}>Low</option>
<option value="medium" {% if request.form.get('priority') == 'medium' or not request.form.get('priority') %}selected{% endif %}>Medium</option>
<option value="high" {% if request.form.get('priority') == 'high' %}selected{% endif %}>High</option>
<option value="urgent" {% if request.form.get('priority') == 'urgent' %}selected{% endif %}>Urgent</option>
</select>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="due_date" class="form-label">Due Date</label>
<input type="date" class="form-control" id="due_date" name="due_date"
value="{{ request.form.get('due_date', '') }}">
<div class="form-text">When should this task be completed?</div>
</div>
<label for="status" class="form-label fw-semibold">
<i class="fas fa-tasks me-2 text-info"></i>Initial Status
</label>
<select class="form-select" id="status" name="status">
<option value="todo" {% if request.form.get('status') == 'todo' or not request.form.get('status') %}selected{% endif %}>To Do</option>
<option value="in_progress" {% if request.form.get('status') == 'in_progress' %}selected{% endif %}>In Progress</option>
<option value="review" {% if request.form.get('status') == 'review' %}selected{% endif %}>Review</option>
<option value="done" {% if request.form.get('status') == 'done' %}selected{% endif %}>Done</option>
<option value="cancelled" {% if request.form.get('status') == 'cancelled' %}selected{% endif %}>Cancelled</option>
</select>
</div>
</div>
<div class="mb-3">
<label for="assigned_to" class="form-label">Assign To</label>
<!-- Due Date and Estimated Hours -->
<div class="row mb-4">
<div class="col-md-6">
<label for="due_date" class="form-label fw-semibold">
<i class="fas fa-calendar me-2 text-secondary"></i>Due Date
</label>
<input type="date" class="form-control" id="due_date" name="due_date"
value="{{ request.form.get('due_date', '') }}">
<small class="form-text text-muted">Optional: Set a deadline for this task</small>
</div>
<div class="col-md-6">
<label for="estimated_hours" class="form-label fw-semibold">
<i class="fas fa-clock me-2 text-warning"></i>Estimated Hours
</label>
<input type="number" class="form-control" id="estimated_hours" name="estimated_hours"
value="{{ request.form.get('estimated_hours', '') }}" step="0.5" min="0" placeholder="0.0">
<small class="form-text text-muted">Optional: Estimate how long this task will take</small>
</div>
</div>
<!-- Assignment -->
<div class="mb-4">
<label for="assigned_to" class="form-label fw-semibold">
<i class="fas fa-user me-2 text-info"></i>Assign To
</label>
<select class="form-select" id="assigned_to" name="assigned_to">
<option value="">Unassigned</option>
{% for user in users %}
@@ -85,71 +130,342 @@
</option>
{% endfor %}
</select>
<div class="form-text">Who will be responsible for this task?</div>
<small class="form-text text-muted">Optional: Assign this task to a team member</small>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('tasks.list_tasks') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Create Task
<!-- Form Actions -->
<div class="d-flex gap-3 pt-3 border-top">
<button type="submit" class="btn btn-primary btn-lg">
<i class="fas fa-save me-2"></i>Create Task
</button>
<a href="{{ url_for('tasks.list_tasks') }}" class="btn btn-outline-secondary btn-lg">
<i class="fas fa-times me-2"></i>Cancel
</a>
</div>
</form>
</div>
</div>
</div>
<!-- Sidebar Help -->
<div class="col-lg-4">
<!-- Task Creation Tips -->
<div class="card mobile-card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-lightbulb me-2 text-warning"></i>Task Creation Tips
</h6>
</div>
<div class="card-body">
<div class="tip-item mb-3">
<div class="d-flex align-items-start">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-check text-primary fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">Clear Naming</small>
<small class="text-muted">Use action verbs and be specific about what needs to be done</small>
</div>
</div>
</div>
<div class="tip-item mb-3">
<div class="d-flex align-items-start">
<div class="bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-clock text-success fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">Realistic Estimates</small>
<small class="text-muted">Consider complexity and dependencies when estimating time</small>
</div>
</div>
</div>
<div class="tip-item mb-3">
<div class="d-flex align-items-start">
<div class="bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-calendar text-info fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">Set Deadlines</small>
<small class="text-muted">Due dates help prioritize work and track progress</small>
</div>
</div>
</div>
<div class="tip-item">
<div class="d-flex align-items-start">
<div class="bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-flag text-warning fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">Priority Matters</small>
<small class="text-muted">Use priority levels to help team members focus on what's most important</small>
</div>
</div>
</div>
</div>
</div>
<!-- Priority Guide -->
<div class="card mobile-card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2 text-info"></i>Priority Guide
</h6>
</div>
<div class="card-body">
<div class="priority-guide-item mb-3">
<div class="d-flex align-items-center mb-2">
<span class="priority-badge priority-low me-2">Low</span>
<small class="text-muted">Non-urgent, can be done later</small>
</div>
</div>
<div class="priority-guide-item mb-3">
<div class="d-flex align-items-center mb-2">
<span class="priority-badge priority-medium me-2">Medium</span>
<small class="text-muted">Normal priority, standard timeline</small>
</div>
</div>
<div class="priority-guide-item mb-3">
<div class="d-flex align-items-center mb-2">
<span class="priority-badge priority-high me-2">High</span>
<small class="text-muted">Important, needs attention soon</small>
</div>
</div>
<div class="priority-guide-item">
<div class="d-flex align-items-center mb-2">
<span class="priority-badge priority-urgent me-2">Urgent</span>
<small class="text-muted">Critical, immediate attention required</small>
</div>
</div>
</div>
</div>
<!-- Status Guide -->
<div class="card mobile-card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-list me-2 text-secondary"></i>Status Guide
</h6>
</div>
<div class="card-body">
<div class="status-guide-item mb-3">
<div class="d-flex align-items-center mb-2">
<span class="status-badge status-todo me-2">To Do</span>
<small class="text-muted">Task is planned but not started</small>
</div>
</div>
<div class="status-guide-item mb-3">
<div class="d-flex align-items-center mb-2">
<span class="status-badge status-in_progress me-2">In Progress</span>
<small class="text-muted">Work has begun on the task</small>
</div>
</div>
<div class="status-guide-item mb-3">
<div class="d-flex align-items-center mb-2">
<span class="status-badge status-review me-2">Review</span>
<small class="text-muted">Task is ready for review/testing</small>
</div>
</div>
<div class="status-guide-item">
<div class="d-flex align-items-center mb-2">
<span class="status-badge status-done me-2">Done</span>
<small class="text-muted">Task is completed successfully</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Set default due date to 1 week from today
const dueDateInput = document.getElementById('due_date');
if (!dueDateInput.value) {
const today = new Date();
const nextWeek = new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000);
dueDateInput.value = nextWeek.toISOString().split('T')[0];
}
// Priority color coding
const prioritySelect = document.getElementById('priority');
const updatePriorityStyle = () => {
const priority = prioritySelect.value;
prioritySelect.className = 'form-select priority-' + priority;
};
prioritySelect.addEventListener('change', updatePriorityStyle);
updatePriorityStyle();
});
</script>
<style>
/* Priority Badges */
.priority-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.priority-low {
border-color: #28a745;
color: #28a745;
background-color: #dcfce7;
color: #166534;
}
.priority-medium {
border-color: #ffc107;
color: #ffc107;
background-color: #fef3c7;
color: #92400e;
}
.priority-high {
border-color: #fd7e14;
color: #fd7e14;
background-color: #fed7aa;
color: #c2410c;
}
.priority-urgent {
border-color: #dc3545;
color: #dc3545;
background-color: #fee2e2;
color: #991b1b;
}
/* Status Badges */
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-todo {
background-color: #e2e8f0;
color: #475569;
}
.status-in_progress {
background-color: #fef3c7;
color: #92400e;
}
.status-review {
background-color: #dbeafe;
color: #1e40af;
}
.status-done {
background-color: #dcfce7;
color: #166534;
}
/* Tip Items */
.tip-item {
padding: 0.75rem;
border-radius: 8px;
background-color: #f8fafc;
transition: all 0.2s ease;
}
.tip-item:hover {
background-color: #f1f5f9;
transform: translateX(4px);
}
/* Priority and Status Guide Items */
.priority-guide-item,
.status-guide-item {
padding: 0.5rem;
border-radius: 6px;
transition: all 0.2s ease;
}
.priority-guide-item:hover,
.status-guide-item:hover {
background-color: #f8fafc;
}
/* Form Styling */
.form-control:focus,
.form-select:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
}
.form-control-lg {
min-height: 56px;
}
.form-select-lg {
min-height: 56px;
}
/* Mobile Optimizations */
@media (max-width: 768px) {
.card-header {
padding: 1rem 1rem 0.75rem 1rem;
}
.card-body {
padding: 0.75rem 1rem;
}
.tip-item:hover {
transform: none;
}
.priority-guide-item:hover,
.status-guide-item:hover {
background-color: transparent;
}
}
/* Hover Effects */
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.tip-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
</style>
<script>
// Form validation and enhancement
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('createTaskForm');
const nameInput = document.getElementById('name');
const descriptionInput = document.getElementById('description');
// Auto-resize description textarea
if (descriptionInput) {
descriptionInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
}
// Form submission enhancement
form.addEventListener('submit', function(e) {
const name = nameInput.value.trim();
if (!name) {
e.preventDefault();
nameInput.focus();
nameInput.classList.add('is-invalid');
return false;
}
// Show loading state
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Creating...';
submitBtn.disabled = true;
// Re-enable after a delay (in case of validation errors)
setTimeout(() => {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}, 5000);
});
// Real-time validation feedback
nameInput.addEventListener('input', function() {
if (this.value.trim()) {
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
this.classList.remove('is-valid');
}
});
});
</script>
{% endblock %}

View File

@@ -1,82 +1,127 @@
{% extends "base.html" %}
{% block title %}Edit Task - Time Tracker{% endblock %}
{% block title %}Edit Task - {{ task.name }} - Time Tracker{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<div class="card mobile-card">
<div class="card-body py-4">
<div class="d-flex align-items-center">
<div class="bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 48px; height: 48px;">
<i class="fas fa-edit text-warning fa-lg"></i>
</div>
<div>
<h1 class="h2 mb-1">Edit Task</h1>
<p class="text-muted mb-0">Update task details and settings for "{{ task.name }}"</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Task Form -->
<div class="row">
<div class="col-lg-8">
<div class="card mobile-card">
<div class="card-header">
<h2 class="mb-0">
<i class="fas fa-edit"></i> Edit Task: {{ task.name }}
</h2>
<h6 class="mb-0">
<i class="fas fa-edit me-2 text-warning"></i>Task Information
</h6>
</div>
<div class="card-body">
<form method="POST">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="name" class="form-label">Task Name *</label>
<input type="text" class="form-control" id="name" name="name"
value="{{ task.name }}" required>
<div class="form-text">Give your task a clear, descriptive name</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="priority" class="form-label">Priority</label>
<select class="form-select" id="priority" name="priority">
<option value="low" {% if task.priority == 'low' %}selected{% endif %}>Low</option>
<option value="medium" {% if task.priority == 'medium' %}selected{% endif %}>Medium</option>
<option value="high" {% if task.priority == 'high' %}selected{% endif %}>High</option>
<option value="urgent" {% if task.priority == 'urgent' %}selected{% endif %}>Urgent</option>
</select>
</div>
</div>
<form method="POST" id="editTaskForm">
<!-- Task Name -->
<div class="mb-4">
<label for="name" class="form-label fw-semibold">
<i class="fas fa-tag me-2 text-primary"></i>Task Name <span class="text-danger">*</span>
</label>
<input type="text" class="form-control form-control-lg" id="name" name="name"
value="{{ task.name }}" placeholder="Enter a descriptive task name" required>
<small class="form-text text-muted">Choose a clear, descriptive name that explains what needs to be done</small>
</div>
<div class="mb-3">
<label for="project_id" class="form-label">Project *</label>
<select class="form-select" id="project_id" name="project_id" required>
<!-- Description -->
<div class="mb-4">
<label for="description" class="form-label fw-semibold">
<i class="fas fa-align-left me-2 text-primary"></i>Description
</label>
<textarea class="form-control" id="description" name="description" rows="4"
placeholder="Provide detailed information about the task, requirements, and any specific instructions...">{{ task.description or '' }}</textarea>
<small class="form-text text-muted">Optional: Add context, requirements, or specific instructions for the task</small>
</div>
<!-- Project Selection -->
<div class="mb-4">
<label for="project_id" class="form-label fw-semibold">
<i class="fas fa-project-diagram me-2 text-primary"></i>Project <span class="text-danger">*</span>
</label>
<select class="form-select form-select-lg" id="project_id" name="project_id" required>
<option value="">Select a project</option>
{% for project in projects %}
<option value="{{ project.id }}" {% if task.project_id == project.id %}selected{% endif %}>
{{ project.name }} ({{ project.client }})
{{ project.name }}
</option>
{% endfor %}
</select>
<small class="form-text text-muted">Select the project this task belongs to</small>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="4"
placeholder="Describe what needs to be done...">{{ task.description or '' }}</textarea>
<div class="form-text">Provide details about the task requirements and deliverables</div>
</div>
<div class="row">
<!-- Priority and Status -->
<div class="row mb-4">
<div class="col-md-6">
<div class="mb-3">
<label for="estimated_hours" class="form-label">Estimated Hours</label>
<input type="number" class="form-control" id="estimated_hours" name="estimated_hours"
value="{{ task.estimated_hours or '' }}"
step="0.5" min="0" placeholder="e.g., 8.5">
<div class="form-text">How long do you think this task will take?</div>
</div>
<label for="priority" class="form-label fw-semibold">
<i class="fas fa-flag me-2 text-warning"></i>Priority
</label>
<select class="form-select" id="priority" name="priority">
<option value="low" {% if task.priority == 'low' %}selected{% endif %}>Low</option>
<option value="medium" {% if task.priority == 'medium' %}selected{% endif %}>Medium</option>
<option value="high" {% if task.priority == 'high' %}selected{% endif %}>High</option>
<option value="urgent" {% if task.priority == 'urgent' %}selected{% endif %}>Urgent</option>
</select>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="due_date" class="form-label">Due Date</label>
<input type="date" class="form-control" id="due_date" name="due_date"
value="{{ task.due_date.strftime('%Y-%m-%d') if task.due_date else '' }}">
<div class="form-text">When should this task be completed?</div>
</div>
<label for="status" class="form-label fw-semibold">
<i class="fas fa-tasks me-2 text-info"></i>Status
</label>
<select class="form-select" id="status" name="status">
<option value="todo" {% if task.status == 'todo' %}selected{% endif %}>To Do</option>
<option value="in_progress" {% if task.status == 'in_progress' %}selected{% endif %}>In Progress</option>
<option value="review" {% if task.status == 'review' %}selected{% endif %}>Review</option>
<option value="done" {% if task.status == 'done' %}selected{% endif %}>Done</option>
<option value="cancelled" {% if task.status == 'cancelled' %}selected{% endif %}>Cancelled</option>
</select>
</div>
</div>
<div class="mb-3">
<label for="assigned_to" class="form-label">Assign To</label>
<!-- Due Date and Estimated Hours -->
<div class="row mb-4">
<div class="col-md-6">
<label for="due_date" class="form-label fw-semibold">
<i class="fas fa-calendar me-2 text-secondary"></i>Due Date
</label>
<input type="date" class="form-control" id="due_date" name="due_date"
value="{{ task.due_date.strftime('%Y-%m-%d') if task.due_date else '' }}">
<small class="form-text text-muted">Optional: Set a deadline for this task</small>
</div>
<div class="col-md-6">
<label for="estimated_hours" class="form-label fw-semibold">
<i class="fas fa-clock me-2 text-warning"></i>Estimated Hours
</label>
<input type="number" class="form-control" id="estimated_hours" name="estimated_hours"
value="{{ task.estimated_hours or '' }}" step="0.5" min="0" placeholder="0.0">
<small class="form-text text-muted">Optional: Estimate how long this task will take</small>
</div>
</div>
<!-- Assignment -->
<div class="mb-4">
<label for="assigned_to" class="form-label fw-semibold">
<i class="fas fa-user me-2 text-info"></i>Assign To
</label>
<select class="form-select" id="assigned_to" name="assigned_to">
<option value="">Unassigned</option>
{% for user in users %}
@@ -85,63 +130,396 @@
</option>
{% endfor %}
</select>
<div class="form-text">Who will be responsible for this task?</div>
<small class="form-text text-muted">Optional: Assign this task to a team member</small>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> Update Task
<!-- Form Actions -->
<div class="d-flex gap-3 pt-3 border-top">
<button type="submit" class="btn btn-warning btn-lg">
<i class="fas fa-save me-2"></i>Update Task
</button>
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="btn btn-outline-secondary btn-lg">
<i class="fas fa-times me-2"></i>Cancel
</a>
</div>
</form>
</div>
</div>
</div>
<!-- Sidebar Information -->
<div class="col-lg-4">
<!-- Current Task Info -->
<div class="card mobile-card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2 text-info"></i>Current Task Info
</h6>
</div>
<div class="card-body">
<div class="task-info-item mb-3">
<small class="text-muted d-block mb-1">Current Status</small>
<span class="status-badge status-{{ task.status }}">
{{ task.status_display }}
</span>
</div>
<div class="task-info-item mb-3">
<small class="text-muted d-block mb-1">Current Priority</small>
<span class="priority-badge priority-{{ task.priority }}">
{{ task.priority_display }}
</span>
</div>
<div class="task-info-item mb-3">
<small class="text-muted d-block mb-1">Project</small>
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-project-diagram text-primary fa-xs"></i>
</div>
<span>{{ task.project.name }}</span>
</div>
</div>
{% if task.assigned_user %}
<div class="task-info-item mb-3">
<small class="text-muted d-block mb-1">Currently Assigned To</small>
<div class="d-flex align-items-center">
<div class="bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height:24px;">
<i class="fas fa-user text-info fa-xs"></i>
</div>
<span>{{ task.assigned_user.username }}</span>
</div>
</div>
{% endif %}
{% if task.due_date %}
<div class="task-info-item mb-3">
<small class="text-muted d-block mb-1">Current Due Date</small>
<div class="d-flex align-items-center">
<div class="bg-{% if task.is_overdue %}danger{% else %}secondary{% endif %} bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-calendar text-{% if task.is_overdue %}danger{% else %}secondary{% endif %} fa-xs"></i>
</div>
<span class="{% if task.is_overdue %}text-danger fw-bold{% endif %}">
{{ task.due_date.strftime('%B %d, %Y') }}
</span>
</div>
</div>
{% endif %}
{% if task.estimated_hours %}
<div class="task-info-item mb-3">
<small class="text-muted d-block mb-1">Current Estimate</small>
<div class="d-flex align-items-center">
<div class="bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-clock text-warning fa-xs"></i>
</div>
<span>{{ task.estimated_hours }} hours</span>
</div>
</div>
{% endif %}
{% if task.total_hours > 0 %}
<div class="task-info-item mb-3">
<small class="text-muted d-block mb-1">Actual Hours</small>
<div class="d-flex align-items-center">
<div class="bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-stopwatch text-success fa-xs"></i>
</div>
<span>{{ task.total_hours }} hours</span>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Quick Actions -->
<div class="card mobile-card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-bolt me-2 text-warning"></i>Quick Actions
</h6>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-eye me-2"></i>View Task
</a>
{% if task.status == 'todo' or task.status == 'in_progress' %}
<a href="{{ url_for('timer.start_timer', project_id=task.project_id, task_id=task.id) }}" class="btn btn-success btn-sm">
<i class="fas fa-play me-2"></i>Start Timer
</a>
{% endif %}
<a href="{{ url_for('tasks.list_tasks') }}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-list me-2"></i>Back to Tasks
</a>
</div>
</div>
</div>
<!-- Edit Tips -->
<div class="card mobile-card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-lightbulb me-2 text-warning"></i>Edit Tips
</h6>
</div>
<div class="card-body">
<div class="tip-item mb-3">
<div class="d-flex align-items-start">
<div class="bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-info text-info fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">Status Changes</small>
<small class="text-muted">Changing status may affect time tracking and progress calculations</small>
</div>
</div>
</div>
<div class="tip-item mb-3">
<div class="d-flex align-items-start">
<div class="bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-exclamation-triangle text-warning fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">Due Date Updates</small>
<small class="text-muted">Consider team workload when adjusting deadlines</small>
</div>
</div>
</div>
<div class="tip-item">
<div class="d-flex align-items-start">
<div class="bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-check text-success fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">Assignment Changes</small>
<small class="text-muted">Notify team members when reassigning tasks</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Priority color coding
const prioritySelect = document.getElementById('priority');
const updatePriorityStyle = () => {
const priority = prioritySelect.value;
prioritySelect.className = 'form-select priority-' + priority;
};
prioritySelect.addEventListener('change', updatePriorityStyle);
updatePriorityStyle();
});
</script>
<style>
/* Priority Badges */
.priority-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.priority-low {
border-color: #28a745;
color: #28a745;
background-color: #dcfce7;
color: #166534;
}
.priority-medium {
border-color: #ffc107;
color: #ffc107;
background-color: #fef3c7;
color: #92400e;
}
.priority-high {
border-color: #fd7e14;
color: #fd7e14;
background-color: #fed7aa;
color: #c2410c;
}
.priority-urgent {
border-color: #dc3545;
color: #dc3545;
background-color: #fee2e2;
color: #991b1b;
}
/* Status Badges */
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-todo {
background-color: #e2e8f0;
color: #475569;
}
.status-in_progress {
background-color: #fef3c7;
color: #92400e;
}
.status-review {
background-color: #dbeafe;
color: #1e40af;
}
.status-done {
background-color: #dcfce7;
color: #166534;
}
.status-cancelled {
background-color: #fee2e2;
color: #991b1b;
}
/* Task Info Items */
.task-info-item {
padding: 0.75rem;
border-radius: 8px;
background-color: #f8fafc;
transition: all 0.2s ease;
}
.task-info-item:hover {
background-color: #f1f5f9;
transform: translateX(4px);
}
/* Tip Items */
.tip-item {
padding: 0.75rem;
border-radius: 8px;
background-color: #f8fafc;
transition: all 0.2s ease;
}
.tip-item:hover {
background-color: #f1f5f9;
transform: translateX(4px);
}
/* Form Styling */
.form-control:focus,
.form-select:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
}
.form-control-lg {
min-height: 56px;
}
.form-select-lg {
min-height: 56px;
}
/* Mobile Optimizations */
@media (max-width: 768px) {
.card-header {
padding: 1rem 1rem 0.75rem 1rem;
}
.card-body {
padding: 0.75rem 1rem;
}
.task-info-item:hover,
.tip-item:hover {
transform: none;
}
}
/* Hover Effects */
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.task-info-item:hover,
.tip-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
</style>
<script>
// Form validation and enhancement
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('editTaskForm');
const nameInput = document.getElementById('name');
const descriptionInput = document.getElementById('description');
// Auto-resize description textarea
if (descriptionInput) {
descriptionInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
}
// Form submission enhancement
form.addEventListener('submit', function(e) {
const name = nameInput.value.trim();
if (!name) {
e.preventDefault();
nameInput.focus();
nameInput.classList.add('is-invalid');
return false;
}
// Show loading state
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Updating...';
submitBtn.disabled = true;
// Re-enable after a delay (in case of validation errors)
setTimeout(() => {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}, 5000);
});
// Real-time validation feedback
nameInput.addEventListener('input', function() {
if (this.value.trim()) {
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
this.classList.remove('is-valid');
}
});
// Highlight current values
const currentStatus = '{{ task.status }}';
const currentPriority = '{{ task.priority }}';
// Add visual indicators for current values
const statusSelect = document.getElementById('status');
const prioritySelect = document.getElementById('priority');
if (statusSelect) {
statusSelect.addEventListener('change', function() {
this.classList.remove('border-success', 'border-warning');
if (this.value === currentStatus) {
this.classList.add('border-success');
} else {
this.classList.add('border-warning');
}
});
}
if (prioritySelect) {
prioritySelect.addEventListener('change', function() {
this.classList.remove('border-success', 'border-warning');
if (this.value === currentPriority) {
this.classList.add('border-success');
} else {
this.classList.add('border-warning');
}
});
}
});
</script>
{% endblock %}

View File

@@ -4,33 +4,109 @@
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Tasks</h1>
<div>
<a href="{{ url_for('tasks.create_task') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> New Task
</a>
<a href="{{ url_for('tasks.my_tasks') }}" class="btn btn-outline-secondary">
<i class="fas fa-user"></i> My Tasks
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('tasks.overdue_tasks') }}" class="btn btn-outline-warning">
<i class="fas fa-exclamation-triangle"></i> Overdue
</a>
{% endif %}
<!-- Header Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card mobile-card">
<div class="card-body py-4">
<div class="row align-items-center">
<div class="col-lg-8 col-md-7 mb-3 mb-md-0">
<div class="d-flex align-items-center mb-2">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 48px; height: 48px;">
<i class="fas fa-tasks text-primary fa-lg"></i>
</div>
<div>
<h2 class="mb-1">Task Management</h2>
<p class="text-muted mb-0">Organize and track your project tasks efficiently</p>
</div>
</div>
</div>
<div class="col-lg-4 col-md-5 text-center text-md-end">
<div class="d-flex flex-column flex-sm-row gap-2 justify-content-center justify-content-md-end">
<a href="{{ url_for('tasks.create_task') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>New Task
</a>
<div class="dropdown d-inline-block">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-filter me-2"></i>Views
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('tasks.my_tasks') }}">
<i class="fas fa-user me-2"></i>My Tasks
</a></li>
{% if current_user.is_admin %}
<li><a class="dropdown-item" href="{{ url_for('tasks.overdue_tasks') }}">
<i class="fas fa-exclamation-triangle me-2"></i>Overdue
</a></li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Stats -->
<div class="row mb-4">
<div class="col-12">
<div class="row g-3">
<div class="col-6 col-md-3">
<div class="card mobile-card bg-primary bg-opacity-10 border-primary border-opacity-25">
<div class="card-body text-center py-3">
<div class="h4 text-primary mb-1">{{ tasks|selectattr('status', 'equalto', 'todo')|list|length }}</div>
<small class="text-muted">To Do</small>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card mobile-card bg-warning bg-opacity-10 border-warning border-opacity-25">
<div class="card-body text-center py-3">
<div class="h4 text-warning mb-1">{{ tasks|selectattr('status', 'equalto', 'in_progress')|list|length }}</div>
<small class="text-muted">In Progress</small>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card mobile-card bg-info bg-opacity-10 border-info border-opacity-25">
<div class="card-body text-center py-3">
<div class="h4 text-info mb-1">{{ tasks|selectattr('status', 'equalto', 'review')|list|length }}</div>
<small class="text-muted">Review</small>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card mobile-card bg-success bg-opacity-10 border-success border-opacity-25">
<div class="card-body text-center py-3">
<div class="h4 text-success mb-1">{{ tasks|selectattr('status', 'equalto', 'done')|list|length }}</div>
<small class="text-muted">Completed</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card mobile-card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-filter me-2 text-muted"></i>Filter Tasks
</h6>
</div>
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-3">
<div class="col-md-4 col-sm-6">
<label for="search" class="form-label">Search</label>
<input type="text" class="form-control" id="search" name="search"
value="{{ search }}" placeholder="Task name or description">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" id="search" name="search"
value="{{ search }}" placeholder="Task name or description">
</div>
</div>
<div class="col-md-2">
<div class="col-md-2 col-sm-6">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="">All Statuses</option>
@@ -41,7 +117,7 @@
<option value="cancelled" {% if status == 'cancelled' %}selected{% endif %}>Cancelled</option>
</select>
</div>
<div class="col-md-2">
<div class="col-md-2 col-sm-6">
<label for="priority" class="form-label">Priority</label>
<select class="form-select" id="priority" name="priority">
<option value="">All Priorities</option>
@@ -51,7 +127,7 @@
<option value="urgent" {% if priority == 'urgent' %}selected{% endif %}>Urgent</option>
</select>
</div>
<div class="col-md-2">
<div class="col-md-2 col-sm-6">
<label for="project_id" class="form-label">Project</label>
<select class="form-select" id="project_id" name="project_id">
<option value="">All Projects</option>
@@ -62,7 +138,7 @@
{% endfor %}
</select>
</div>
<div class="col-md-2">
<div class="col-md-2 col-sm-6">
<label for="assigned_to" class="form-label">Assigned To</label>
<select class="form-select" id="assigned_to" name="assigned_to">
<option value="">All Users</option>
@@ -73,85 +149,150 @@
{% endfor %}
</select>
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-search"></i>
</button>
<div class="col-12">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search me-2"></i>Apply Filters
</button>
<a href="{{ url_for('tasks.list_tasks') }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Clear
</a>
</div>
</div>
</form>
</div>
</div>
<!-- Tasks List -->
<!-- Tasks Grid -->
{% if tasks %}
<div class="row">
<div class="row g-4">
{% for task in tasks %}
<div class="col-md-6 col-lg-4 mb-3">
<div class="card h-100 task-card {{ task.priority_class }}">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="badge bg-{{ 'success' if task.status == 'done' else 'warning' if task.status == 'in_progress' else 'info' if task.status == 'review' else 'secondary' }}">
{{ task.status_display }}
</span>
<span class="badge priority-badge priority-{{ task.priority }}">
{{ task.priority_display }}
</span>
<div class="col-xl-4 col-lg-6 col-md-6">
<div class="card mobile-card task-card h-100 {{ task.priority_class }}">
<!-- Card Header -->
<div class="card-header border-0 pb-0">
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="d-flex align-items-center">
<span class="status-badge status-{{ task.status }} me-2">
{{ task.status_display }}
</span>
<span class="priority-badge priority-{{ task.priority }}">
{{ task.priority_display }}
</span>
</div>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary border-0" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{{ url_for('tasks.view_task', task_id=task.id) }}">
<i class="fas fa-eye me-2"></i>View Details
</a></li>
{% if current_user.is_admin or task.created_by == current_user.id %}
<li><a class="dropdown-item" href="{{ url_for('tasks.edit_task', task_id=task.id) }}">
<i class="fas fa-edit me-2"></i>Edit Task
</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('timer.start_timer', project_id=task.project_id, task_id=task.id) }}">
<i class="fas fa-play me-2"></i>Start Timer
</a></li>
</ul>
</div>
</div>
</div>
<div class="card-body">
<h5 class="card-title">
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="text-decoration-none">
<!-- Card Body -->
<div class="card-body pt-0">
<h5 class="card-title mb-2">
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="text-decoration-none text-dark">
{{ task.name }}
</a>
</h5>
{% if task.description %}
<p class="card-text text-muted">{{ task.description[:100] }}{% if task.description|length > 100 %}...{% endif %}</p>
<p class="card-text text-muted small mb-3">
{{ task.description[:120] }}{% if task.description|length > 120 %}...{% endif %}
</p>
{% endif %}
<div class="task-meta">
<small class="text-muted">
<i class="fas fa-project-diagram"></i> {{ task.project.name }}
</small>
<!-- Project Info -->
<div class="d-flex align-items-center mb-3">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-project-diagram text-primary fa-xs"></i>
</div>
<span class="text-muted small">{{ task.project.name }}</span>
</div>
<!-- Task Meta -->
<div class="task-meta mb-3">
{% if task.assigned_user %}
<br><small class="text-muted">
<i class="fas fa-user"></i> {{ task.assigned_user.username }}
</small>
<div class="d-flex align-items-center mb-2">
<div class="bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 20px; height: 20px;">
<i class="fas fa-user text-info fa-xs"></i>
</div>
<span class="text-muted small">{{ task.assigned_user.username }}</span>
</div>
{% endif %}
{% if task.due_date %}
<br><small class="text-muted {% if task.is_overdue %}text-danger{% endif %}">
<i class="fas fa-calendar"></i> Due: {{ task.due_date.strftime('%Y-%m-%d') }}
{% if task.is_overdue %}<i class="fas fa-exclamation-triangle text-warning"></i>{% endif %}
</small>
<div class="d-flex align-items-center mb-2">
<div class="bg-{% if task.is_overdue %}danger{% else %}secondary{% endif %} bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 20px; height: 20px;">
<i class="fas fa-calendar text-{% if task.is_overdue %}danger{% else %}secondary{% endif %} fa-xs"></i>
</div>
<span class="text-muted small {% if task.is_overdue %}text-danger fw-bold{% endif %}">
Due: {{ task.due_date.strftime('%b %d, %Y') }}
{% if task.is_overdue %}<i class="fas fa-exclamation-triangle text-warning ms-1"></i>{% endif %}
</span>
</div>
{% endif %}
{% if task.estimated_hours %}
<br><small class="text-muted">
<i class="fas fa-clock"></i> Est: {{ task.estimated_hours }}h
</small>
<div class="d-flex align-items-center mb-2">
<div class="bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 20px; height: 20px;">
<i class="fas fa-clock text-warning fa-xs"></i>
</div>
<span class="text-muted small">Est: {{ task.estimated_hours }}h</span>
</div>
{% endif %}
{% if task.total_hours > 0 %}
<br><small class="text-muted">
<i class="fas fa-stopwatch"></i> Actual: {{ task.total_hours }}h
</small>
<div class="d-flex align-items-center mb-2">
<div class="bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 20px; height: 20px;">
<i class="fas fa-stopwatch text-success fa-xs"></i>
</div>
<span class="text-muted small">Actual: {{ task.total_hours }}h</span>
</div>
{% endif %}
</div>
<!-- Progress Bar -->
{% if task.estimated_hours and task.total_hours > 0 %}
<div class="progress mt-2" style="height: 8px;">
<div class="progress-bar" role="progressbar"
style="width: {{ task.progress_percentage }}%"
aria-valuenow="{{ task.progress_percentage }}"
aria-valuemin="0" aria-valuemax="100">
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<small class="text-muted">Progress</small>
<small class="text-muted fw-bold">{{ task.progress_percentage }}%</small>
</div>
<div class="progress" style="height: 6px;">
<div class="progress-bar bg-primary" role="progressbar"
style="width: {{ task.progress_percentage }}%"
aria-valuenow="{{ task.progress_percentage }}"
aria-valuemin="0" aria-valuemax="100">
</div>
</div>
</div>
<small class="text-muted">{{ task.progress_percentage }}% complete</small>
{% endif %}
</div>
<div class="card-footer">
<div class="btn-group btn-group-sm w-100" role="group">
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="btn btn-outline-primary">
<i class="fas fa-eye"></i> View
<!-- Card Footer -->
<div class="card-footer border-0 pt-0">
<div class="d-grid gap-2">
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-eye me-2"></i>View Details
</a>
{% if current_user.is_admin or task.created_by == current_user.id %}
<a href="{{ url_for('tasks.edit_task', task_id=task.id) }}" class="btn btn-outline-secondary">
<i class="fas fa-edit"></i> Edit
{% if task.status == 'todo' or task.status == 'in_progress' %}
<a href="{{ url_for('timer.start_timer', project_id=task.project_id, task_id=task.id) }}" class="btn btn-success btn-sm">
<i class="fas fa-play me-2"></i>Start Timer
</a>
{% endif %}
</div>
@@ -163,11 +304,13 @@
<!-- Pagination -->
{% if pagination.pages > 1 %}
<nav aria-label="Task pagination" class="mt-4">
<nav aria-label="Task pagination" class="mt-5">
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('tasks.list_tasks', page=pagination.prev_num, **request.args) }}">Previous</a>
<a class="page-link" href="{{ url_for('tasks.list_tasks', page=pagination.prev_num, **request.args) }}">
<i class="fas fa-chevron-left"></i> Previous
</a>
</li>
{% endif %}
@@ -191,66 +334,206 @@
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('tasks.list_tasks', page=pagination.next_num, **request.args) }}">Next</a>
<a class="page-link" href="{{ url_for('tasks.list_tasks', page=pagination.next_num, **request.args) }}">
Next <i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-tasks fa-3x text-muted mb-3"></i>
<h3 class="text-muted">No tasks found</h3>
<p class="text-muted">Try adjusting your filters or create a new task.</p>
<a href="{{ url_for('tasks.create_task') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> Create Your First Task
</a>
<!-- Empty State -->
<div class="row">
<div class="col-12">
<div class="card mobile-card">
<div class="card-body text-center py-5">
<div class="bg-light rounded-circle d-flex align-items-center justify-content-center mx-auto mb-4" style="width: 80px; height: 80px;">
<i class="fas fa-tasks fa-2x text-muted"></i>
</div>
<h3 class="text-muted mb-3">No tasks found</h3>
<p class="text-muted mb-4">Try adjusting your filters or create your first task to get started.</p>
<div class="d-flex flex-column flex-sm-row gap-2 justify-content-center">
<a href="{{ url_for('tasks.create_task') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Create Your First Task
</a>
<a href="{{ url_for('tasks.list_tasks') }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Clear Filters
</a>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<style>
/* Task Card Styling */
.task-card {
transition: transform 0.2s, box-shadow 0.2s;
transition: all 0.3s ease;
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.task-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
border-color: var(--primary-color);
}
.priority-badge {
/* Status Badges */
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-todo {
background-color: #e2e8f0;
color: #475569;
}
.status-in_progress {
background-color: #fef3c7;
color: #92400e;
}
.status-review {
background-color: #dbeafe;
color: #1e40af;
}
.status-done {
background-color: #dcfce7;
color: #166534;
}
.status-cancelled {
background-color: #fee2e2;
color: #991b1b;
}
/* Priority Badges */
.priority-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.priority-low {
background-color: #28a745 !important;
background-color: #dcfce7;
color: #166534;
}
.priority-medium {
background-color: #ffc107 !important;
color: #212529 !important;
background-color: #fef3c7;
color: #92400e;
}
.priority-high {
background-color: #fd7e14 !important;
background-color: #fed7aa;
color: #c2410c;
}
.priority-urgent {
background-color: #dc3545 !important;
background-color: #fee2e2;
color: #991b1b;
}
.task-meta small {
display: block;
margin-bottom: 0.25rem;
/* Priority Card Borders */
.task-card.priority-low {
border-left: 4px solid #22c55e;
}
.task-card.priority-medium {
border-left: 4px solid #eab308;
}
.task-card.priority-high {
border-left: 4px solid #f97316;
}
.task-card.priority-urgent {
border-left: 4px solid #ef4444;
}
/* Task Meta Styling */
.task-meta .d-flex {
transition: all 0.2s ease;
}
.task-meta .d-flex:hover {
transform: translateX(4px);
}
/* Progress Bar */
.progress {
background-color: #e9ecef;
background-color: #f1f5f9;
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
background-color: #007bff;
background: linear-gradient(90deg, var(--primary-color) 0%, var(--primary-dark) 100%);
border-radius: 10px;
}
/* Mobile Optimizations */
@media (max-width: 768px) {
.task-card {
margin-bottom: 1rem;
}
.card-header {
padding: 1rem 1rem 0.5rem 1rem;
}
.card-body {
padding: 0.75rem 1rem;
}
.card-footer {
padding: 0.75rem 1rem 1rem 1rem;
}
.status-badge, .priority-badge {
font-size: 0.7rem;
padding: 0.2rem 0.6rem;
}
}
/* Hover Effects */
.task-card .card-title a:hover {
color: var(--primary-color) !important;
}
.task-card .btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
/* Animation for stats cards */
.card.bg-primary.bg-opacity-10,
.card.bg-warning.bg-opacity-10,
.card.bg-info.bg-opacity-10,
.card.bg-success.bg-opacity-10 {
transition: all 0.3s ease;
}
.card.bg-primary.bg-opacity-10:hover,
.card.bg-warning.bg-opacity-10:hover,
.card.bg-info.bg-opacity-10:hover,
.card.bg-success.bg-opacity-10:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
</style>
{% endblock %}

View File

@@ -4,195 +4,535 @@
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>My Tasks</h1>
<div>
<a href="{{ url_for('tasks.create_task') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> New Task
</a>
<a href="{{ url_for('tasks.list_tasks') }}" class="btn btn-outline-secondary">
<i class="fas fa-list"></i> All Tasks
</a>
</div>
</div>
<!-- Status Filter -->
<div class="card mb-4">
<div class="card-body">
<div class="d-flex gap-2 flex-wrap">
<a href="{{ url_for('tasks.my_tasks') }}"
class="btn btn-{{ 'primary' if not status else 'outline-primary' }}">
All Tasks
</a>
<a href="{{ url_for('tasks.my_tasks', status='todo') }}"
class="btn btn-{{ 'secondary' if status == 'todo' else 'outline-secondary' }}">
To Do
</a>
<a href="{{ url_for('tasks.my_tasks', status='in_progress') }}"
class="btn btn-{{ 'warning' if status == 'in_progress' else 'outline-warning' }}">
In Progress
</a>
<a href="{{ url_for('tasks.my_tasks', status='review') }}"
class="btn btn-{{ 'info' if status == 'review' else 'outline-info' }}">
Review
</a>
<a href="{{ url_for('tasks.my_tasks', status='done') }}"
class="btn btn-{{ 'success' if status == 'done' else 'outline-success' }}">
Done
</a>
<!-- Header Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card mobile-card">
<div class="card-body py-4">
<div class="row align-items-center">
<div class="col-lg-8 col-md-7 mb-3 mb-md-0">
<div class="d-flex align-items-center mb-2">
<div class="bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 48px; height: 48px;">
<i class="fas fa-user text-info fa-lg"></i>
</div>
<div>
<h2 class="mb-1">My Tasks</h2>
<p class="text-muted mb-0">Tasks assigned to you and tasks you've created</p>
</div>
</div>
</div>
<div class="col-lg-4 col-md-5 text-center text-md-end">
<div class="d-flex flex-column flex-sm-row gap-2 justify-content-center justify-content-md-end">
<a href="{{ url_for('tasks.create_task') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>New Task
</a>
<a href="{{ url_for('tasks.list_tasks') }}" class="btn btn-outline-secondary">
<i class="fas fa-list me-2"></i>All Tasks
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tasks List -->
{% if tasks %}
<div class="row">
{% for task in tasks %}
<div class="col-md-6 col-lg-4 mb-3">
<div class="card h-100 task-card {{ task.priority_class }}">
<div class="card-header d-flex justify-content-between align-items-center">
<span class="badge bg-{{ 'success' if task.status == 'done' else 'warning' if task.status == 'in_progress' else 'info' if task.status == 'review' else 'secondary' }}">
{{ task.status_display }}
</span>
<span class="badge priority-badge priority-{{ task.priority }}">
{{ task.priority_display }}
</span>
<!-- Quick Stats -->
<div class="row mb-4">
<div class="col-12">
<div class="row g-3">
<div class="col-6 col-md-3">
<div class="card mobile-card bg-primary bg-opacity-10 border-primary border-opacity-25">
<div class="card-body text-center py-3">
<div class="h4 text-primary mb-1">{{ tasks|selectattr('status', 'equalto', 'todo')|list|length }}</div>
<small class="text-muted">To Do</small>
</div>
</div>
</div>
<div class="card-body">
<div class="col-6 col-md-3">
<div class="card mobile-card bg-warning bg-opacity-10 border-warning border-opacity-25">
<div class="card-body text-center py-3">
<div class="h4 text-warning mb-1">{{ tasks|selectattr('status', 'equalto', 'in_progress')|list|length }}</div>
<small class="text-muted">In Progress</small>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card mobile-card bg-info bg-opacity-10 border-info border-opacity-25">
<div class="card-body text-center py-3">
<div class="h4 text-info mb-1">{{ tasks|selectattr('status', 'equalto', 'review')|list|length }}</div>
<small class="text-muted">Review</small>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card mobile-card bg-success bg-opacity-10 border-success border-opacity-25">
<div class="card-body text-center py-3">
<div class="h4 text-success mb-1">{{ tasks|selectattr('status', 'equalto', 'done')|list|length }}</div>
<small class="text-muted">Completed</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="card mobile-card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-filter me-2 text-muted"></i>Filter My Tasks
</h6>
</div>
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-4 col-sm-6">
<label for="search" class="form-label">Search</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" id="search" name="search"
value="{{ search }}" placeholder="Task name or description">
</div>
</div>
<div class="col-md-2 col-sm-6">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="">All Statuses</option>
<option value="todo" {% if status == 'todo' %}selected{% endif %}>To Do</option>
<option value="in_progress" {% if status == 'in_progress' %}selected{% endif %}>In Progress</option>
<option value="review" {% if status == 'review' %}selected{% endif %}>Review</option>
<option value="done" {% if status == 'done' %}selected{% endif %}>Done</option>
<option value="cancelled" {% if status == 'cancelled' %}selected{% endif %}>Cancelled</option>
</select>
</div>
<div class="col-md-2 col-sm-6">
<label for="priority" class="form-label">Priority</label>
<select class="form-select" id="priority" name="priority">
<option value="">All Priorities</option>
<option value="low" {% if priority == 'low' %}selected{% endif %}>Low</option>
<option value="medium" {% if priority == 'medium' %}selected{% endif %}>Medium</option>
<option value="high" {% if priority == 'high' %}selected{% endif %}>High</option>
<option value="urgent" {% if priority == 'urgent' %}selected{% endif %}>Urgent</option>
</select>
</div>
<div class="col-md-2 col-sm-6">
<label for="project_id" class="form-label">Project</label>
<select class="form-select" id="project_id" name="project_id">
<option value="">All Projects</option>
{% for project in projects %}
<option value="{{ project.id }}" {% if project_id == project.id %}selected{% endif %}>
{{ project.name }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 col-sm-6">
<label for="task_type" class="form-label">Task Type</label>
<select class="form-select" id="task_type" name="task_type">
<option value="">All Types</option>
<option value="assigned" {% if task_type == 'assigned' %}selected{% endif %}>Assigned to Me</option>
<option value="created" {% if task_type == 'created' %}selected{% endif %}>Created by Me</option>
</select>
</div>
<div class="col-12">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search me-2"></i>Apply Filters
</button>
<a href="{{ url_for('tasks.my_tasks') }}" class="btn btn-outline-secondary">
<i class="fas fa-times me-2"></i>Clear
</a>
</div>
</div>
</form>
</div>
</div>
<!-- Tasks Grid -->
{% if tasks %}
<div class="row g-4">
{% for task in tasks %}
<div class="col-xl-4 col-lg-6 col-md-6">
<div class="card mobile-card task-card h-100 {{ task.priority_class }}">
<!-- Card Header -->
<div class="card-header border-0 pb-0">
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="d-flex align-items-center">
<span class="status-badge status-{{ task.status }} me-2">
{{ task.status_display }}
</span>
<span class="priority-badge priority-{{ task.priority }}">
{{ task.priority_display }}
</span>
</div>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary border-0" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{{ url_for('tasks.view_task', task_id=task.id) }}">
<i class="fas fa-eye me-2"></i>View Details
</a></li>
{% if current_user.is_admin or task.created_by == current_user.id %}
<li><a class="dropdown-item" href="{{ url_for('tasks.edit_task', task_id=task.id) }}">
<i class="fas fa-edit me-2"></i>Edit Task
</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('timer.start_timer', project_id=task.project_id, task_id=task.id) }}">
<i class="fas fa-play me-2"></i>Start Timer
</a></li>
</ul>
</div>
</div>
</div>
<!-- Card Body -->
<div class="card-body pt-0">
<h5 class="card-title">
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="text-decoration-none">
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="text-decoration-none text-dark">
{{ task.name }}
</a>
</h5>
{% if task.description %}
<p class="card-text text-muted">{{ task.description[:100] }}{% if task.description|length > 100 %}...{% endif %}</p>
<p class="card-text text-muted small mb-3">
{{ task.description[:120] }}{% if task.description|length > 120 %}...{% endif %}
</p>
{% endif %}
<div class="task-meta">
<small class="text-muted">
<i class="fas fa-project-diagram"></i> {{ task.project.name }}
</small>
<br><small class="text-muted">
<i class="fas fa-user"></i>
{% if task.assigned_user %}
{{ task.assigned_user.username }}
{% else %}
Unassigned
{% endif %}
</small>
{% if task.due_date %}
<br><small class="text-muted {% if task.is_overdue %}text-danger{% endif %}">
<i class="fas fa-calendar"></i> Due: {{ task.due_date.strftime('%Y-%m-%d') }}
{% if task.is_overdue %}<i class="fas fa-exclamation-triangle text-warning"></i>{% endif %}
</small>
{% endif %}
{% if task.estimated_hours %}
<br><small class="text-muted">
<i class="fas fa-clock"></i> Est: {{ task.estimated_hours }}h
</small>
{% endif %}
{% if task.total_hours > 0 %}
<br><small class="text-muted">
<i class="fas fa-stopwatch"></i> Actual: {{ task.total_hours }}h
</small>
{% endif %}
<!-- Project Info -->
<div class="d-flex align-items-center mb-3">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-project-diagram text-primary fa-xs"></i>
</div>
<span class="text-muted small">{{ task.project.name }}</span>
</div>
{% if task.estimated_hours and task.total_hours > 0 %}
<div class="progress mt-2" style="height: 8px;">
<div class="progress-bar" role="progressbar"
style="width: {{ task.progress_percentage }}%"
aria-valuenow="{{ task.progress_percentage }}"
aria-valuemin="0" aria-valuemax="100">
<!-- Task Meta -->
<div class="task-meta mb-3">
{% if task.assigned_user %}
<div class="d-flex align-items-center mb-2">
<div class="bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 20px; height: 20px;">
<i class="fas fa-user text-info fa-xs"></i>
</div>
<span class="text-muted small">{{ task.assigned_user.username }}</span>
</div>
{% endif %}
{% if task.due_date %}
<div class="d-flex align-items-center mb-2">
<div class="bg-{% if task.is_overdue %}danger{% else %}secondary{% endif %} bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 20px; height: 20px;">
<i class="fas fa-calendar text-{% if task.is_overdue %}danger{% else %}secondary{% endif %} fa-xs"></i>
</div>
<span class="text-muted small {% if task.is_overdue %}text-danger fw-bold{% endif %}">
Due: {{ task.due_date.strftime('%b %d, %Y') }}
{% if task.is_overdue %}<i class="fas fa-exclamation-triangle text-warning ms-1"></i>{% endif %}
</span>
</div>
{% endif %}
{% if task.estimated_hours %}
<div class="d-flex align-items-center mb-2">
<div class="bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 20px; height: 20px;">
<i class="fas fa-clock text-warning fa-xs"></i>
</div>
<span class="text-muted small">Est: {{ task.estimated_hours }}h</span>
</div>
{% endif %}
{% if task.total_hours > 0 %}
<div class="d-flex align-items-center mb-2">
<div class="bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 20px; height: 20px;">
<i class="fas fa-stopwatch text-success fa-xs"></i>
</div>
<span class="text-muted small">Actual: {{ task.total_hours }}h</span>
</div>
{% endif %}
<!-- Task Type Indicator -->
<div class="d-flex align-items-center mb-2">
<div class="bg-secondary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 20px; height: 20px;">
<i class="fas fa-{% if task.assigned_to == current_user.id %}user-check{% else %}user-plus{% endif %} text-secondary fa-xs"></i>
</div>
<span class="text-muted small">
{% if task.assigned_to == current_user.id %}
Assigned to me
{% else %}
Created by me
{% endif %}
</span>
</div>
</div>
<!-- Progress Bar -->
{% if task.estimated_hours and task.total_hours > 0 %}
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<small class="text-muted">Progress</small>
<small class="text-muted fw-bold">{{ task.progress_percentage }}%</small>
</div>
<div class="progress" style="height: 6px;">
<div class="progress-bar bg-primary" role="progressbar"
style="width: {{ task.progress_percentage }}%"
aria-valuenow="{{ task.progress_percentage }}"
aria-valuemin="0" aria-valuemax="100">
</div>
</div>
</div>
<small class="text-muted">{{ task.progress_percentage }}% complete</small>
{% endif %}
</div>
<div class="card-footer">
<div class="btn-group btn-group-sm w-100" role="group">
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="btn btn-outline-primary">
<i class="fas fa-eye"></i> View
<!-- Card Footer -->
<div class="card-footer border-0 pt-0">
<div class="d-grid gap-2">
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-eye me-2"></i>View Details
</a>
{% if current_user.is_admin or task.created_by == current_user.id %}
<a href="{{ url_for('tasks.edit_task', task_id=task.id) }}" class="btn btn-outline-secondary">
<i class="fas fa-edit"></i> Edit
{% if task.status == 'todo' or task.status == 'in_progress' %}
<a href="{{ url_for('timer.start_timer', project_id=task.project_id, task_id=task.id) }}" class="btn btn-success btn-sm">
<i class="fas fa-play me-2"></i>Start Timer
</a>
{% endif %}
<a href="{{ url_for('timer.start_timer', project_id=task.project.id, task_id=task.id) }}"
class="btn btn-outline-success">
<i class="fas fa-play"></i> Timer
</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-tasks fa-3x text-muted mb-3"></i>
<h3 class="text-muted">No tasks found</h3>
<p class="text-muted">
{% if status %}
You don't have any {{ status.replace('_', ' ') }} tasks.
{% else %}
You don't have any tasks assigned to you or created by you.
<!-- Pagination -->
{% if pagination.pages > 1 %}
<nav aria-label="My tasks pagination" class="mt-5">
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('tasks.my_tasks', page=pagination.prev_num, **request.args) }}">
<i class="fas fa-chevron-left"></i> Previous
</a>
</li>
{% endif %}
</p>
<a href="{{ url_for('tasks.create_task') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> Create Your First Task
</a>
{% for page_num in pagination.iter_pages() %}
{% if page_num %}
{% if page_num != pagination.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('tasks.my_tasks', page=page_num, **request.args) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('tasks.my_tasks', page=pagination.next_num, **request.args) }}">
Next <i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<!-- Empty State -->
<div class="row">
<div class="col-12">
<div class="card mobile-card">
<div class="card-body text-center py-5">
<div class="bg-light rounded-circle d-flex align-items-center justify-content-center mx-auto mb-4" style="width: 80px; height: 80px;">
<i class="fas fa-user fa-2x text-muted"></i>
</div>
<h3 class="text-muted mb-3">No tasks found</h3>
<p class="text-muted mb-4">You don't have any tasks assigned to you or created by you yet.</p>
<div class="d-flex flex-column flex-sm-row gap-2 justify-content-center">
<a href="{{ url_for('tasks.create_task') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Create Your First Task
</a>
<a href="{{ url_for('tasks.list_tasks') }}" class="btn btn-outline-secondary">
<i class="fas fa-list me-2"></i>View All Tasks
</a>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
<style>
/* Task Card Styling */
.task-card {
transition: transform 0.2s, box-shadow 0.2s;
transition: all 0.3s ease;
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.task-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
border-color: var(--primary-color);
}
.priority-badge {
/* Status Badges */
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-todo {
background-color: #e2e8f0;
color: #475569;
}
.status-in_progress {
background-color: #fef3c7;
color: #92400e;
}
.status-review {
background-color: #dbeafe;
color: #1e40af;
}
.status-done {
background-color: #dcfce7;
color: #166534;
}
.status-cancelled {
background-color: #fee2e2;
color: #991b1b;
}
/* Priority Badges */
.priority-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.priority-low {
background-color: #28a745 !important;
background-color: #dcfce7;
color: #166534;
}
.priority-medium {
background-color: #ffc107 !important;
color: #212529 !important;
background-color: #fef3c7;
color: #92400e;
}
.priority-high {
background-color: #fd7e14 !important;
background-color: #fed7aa;
color: #c2410c;
}
.priority-urgent {
background-color: #dc3545 !important;
background-color: #fee2e2;
color: #991b1b;
}
.task-meta small {
display: block;
margin-bottom: 0.25rem;
/* Priority Card Borders */
.task-card.priority-low {
border-left: 4px solid #22c55e;
}
.task-card.priority-medium {
border-left: 4px solid #eab308;
}
.task-card.priority-high {
border-left: 4px solid #f97316;
}
.task-card.priority-urgent {
border-left: 4px solid #ef4444;
}
/* Task Meta Styling */
.task-meta .d-flex {
transition: all 0.2s ease;
}
.task-meta .d-flex:hover {
transform: translateX(4px);
}
/* Progress Bar */
.progress {
background-color: #e9ecef;
background-color: #f1f5f9;
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
background-color: #007bff;
background: linear-gradient(90deg, var(--primary-color) 0%, var(--primary-dark) 100%);
border-radius: 10px;
}
.btn-group .btn {
flex: 1;
/* Mobile Optimizations */
@media (max-width: 768px) {
.task-card {
margin-bottom: 1rem;
}
.card-header {
padding: 1rem 1rem 0.5rem 1rem;
}
.card-body {
padding: 0.75rem 1rem;
}
.card-footer {
padding: 0.75rem 1rem 1rem 1rem;
}
.status-badge, .priority-badge {
font-size: 0.7rem;
padding: 0.2rem 0.6rem;
}
}
/* Hover Effects */
.task-card .card-title a:hover {
color: var(--primary-color) !important;
}
.task-card .btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
/* Animation for stats cards */
.card.bg-primary.bg-opacity-10,
.card.bg-warning.bg-opacity-10,
.card.bg-info.bg-opacity-10,
.card.bg-success.bg-opacity-10 {
transition: all 0.3s ease;
}
.card.bg-primary.bg-opacity-10:hover,
.card.bg-warning.bg-opacity-10:hover,
.card.bg-info.bg-opacity-10:hover,
.card.bg-success.bg-opacity-10:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
</style>
{% endblock %}

View File

@@ -4,384 +4,561 @@
{% block content %}
<div class="container mt-4">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('tasks.list_tasks') }}">Tasks</a></li>
<li class="breadcrumb-item active">{{ task.name }}</li>
</ol>
</nav>
<!-- Task Header -->
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('tasks.list_tasks') }}">Tasks</a></li>
<li class="breadcrumb-item active">{{ task.name }}</li>
</ol>
</nav>
<h1 class="mb-2">{{ task.name }}</h1>
<div class="d-flex gap-2 align-items-center">
<span class="badge bg-{{ 'success' if task.status == 'done' else 'warning' if task.status == 'in_progress' else 'info' if task.status == 'review' else 'secondary' }} fs-6">
{{ task.status_display }}
</span>
<span class="badge priority-badge priority-{{ task.priority }} fs-6">
{{ task.priority_display }}
</span>
{% if task.is_overdue %}
<span class="badge bg-danger fs-6">
<i class="fas fa-exclamation-triangle"></i> Overdue
</span>
{% endif %}
<div class="row mb-4">
<div class="col-12">
<div class="card mobile-card">
<div class="card-body py-4">
<div class="row align-items-start">
<div class="col-lg-8 col-md-7 mb-3 mb-md-0">
<div class="d-flex align-items-start mb-3">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 48px; height: 48px;">
<i class="fas fa-tasks text-primary fa-lg"></i>
</div>
<div class="flex-grow-1">
<h1 class="h2 mb-2">{{ task.name }}</h1>
{% if task.description %}
<p class="text-muted mb-0">{{ task.description }}</p>
{% endif %}
</div>
</div>
<!-- Status and Priority -->
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="status-badge status-{{ task.status }}">
{{ task.status_display }}
</span>
<span class="priority-badge priority-{{ task.priority }}">
{{ task.priority_display }}
</span>
{% if task.is_overdue %}
<span class="badge bg-danger">
<i class="fas fa-exclamation-triangle me-1"></i>Overdue
</span>
{% endif %}
</div>
</div>
<div class="col-lg-4 col-md-5">
<div class="d-flex flex-column gap-2">
{% if task.status == 'todo' or task.status == 'in_progress' %}
<a href="{{ url_for('timer.start_timer', project_id=task.project_id, task_id=task.id) }}" class="btn btn-success">
<i class="fas fa-play me-2"></i>Start Timer
</a>
{% endif %}
{% if current_user.is_admin or task.created_by == current_user.id %}
<a href="{{ url_for('tasks.edit_task', task_id=task.id) }}" class="btn btn-outline-primary">
<i class="fas fa-edit me-2"></i>Edit Task
</a>
{% endif %}
<a href="{{ url_for('tasks.list_tasks') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Back to Tasks
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex gap-2">
{% if current_user.is_admin or task.created_by == current_user.id %}
<a href="{{ url_for('tasks.edit_task', task_id=task.id) }}" class="btn btn-outline-secondary">
<i class="fas fa-edit"></i> Edit
</a>
{% endif %}
<a href="{{ url_for('projects.view_project', project_id=task.project.id) }}" class="btn btn-outline-primary">
<i class="fas fa-project-diagram"></i> View Project
</a>
</div>
</div>
<div class="row">
<!-- Task Details -->
<div class="col-md-8">
<div class="card mb-4">
<!-- Task Details Grid -->
<div class="row g-4">
<!-- Main Task Information -->
<div class="col-lg-8">
<!-- Project Information -->
<div class="card mobile-card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Task Details</h5>
<h6 class="mb-0">
<i class="fas fa-project-diagram me-2 text-primary"></i>Project Information
</h6>
</div>
<div class="card-body">
{% if task.description %}
<div class="mb-3">
<h6>Description</h6>
<p class="mb-0">{{ task.description }}</p>
</div>
{% endif %}
<div class="row">
<div class="col-md-6">
<h6>Project</h6>
<p class="mb-3">
<a href="{{ url_for('projects.view_project', project_id=task.project.id) }}" class="text-decoration-none">
{{ task.project.name }}
</a>
<br><small class="text-muted">{{ task.project.client }}</small>
</p>
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="fas fa-project-diagram text-primary"></i>
</div>
<div class="col-md-6">
<h6>Created By</h6>
<p class="mb-3">{{ task.creator.username }}</p>
<div>
<h6 class="mb-1">{{ task.project.name }}</h6>
<p class="text-muted mb-0">{{ task.project.description or 'No description available' }}</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h6>Assigned To</h6>
<p class="mb-3">
{% if task.assigned_user %}
{{ task.assigned_user.username }}
{% else %}
<span class="text-muted">Unassigned</span>
{% endif %}
</p>
<!-- Time Tracking -->
<div class="card mobile-card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-clock me-2 text-warning"></i>Time Tracking
</h6>
</div>
<div class="card-body">
<div class="row g-3">
{% if task.estimated_hours %}
<div class="col-6 col-md-3">
<div class="text-center">
<div class="h4 text-warning mb-1">{{ task.estimated_hours }}</div>
<small class="text-muted">Estimated Hours</small>
</div>
</div>
<div class="col-md-6">
<h6>Due Date</h6>
<p class="mb-3">
{% if task.due_date %}
<span class="{% if task.is_overdue %}text-danger{% endif %}">
{{ task.due_date.strftime('%Y-%m-%d') }}
{% if task.is_overdue %}
<i class="fas fa-exclamation-triangle text-warning"></i>
{% endif %}
</span>
{% else %}
<span class="text-muted">No due date</span>
{% endif %}
</p>
{% endif %}
{% if task.total_hours > 0 %}
<div class="col-6 col-md-3">
<div class="text-center">
<div class="h4 text-success mb-1">{{ task.total_hours }}</div>
<small class="text-muted">Actual Hours</small>
</div>
</div>
{% endif %}
{% if task.estimated_hours and task.total_hours > 0 %}
<div class="col-6 col-md-3">
<div class="text-center">
<div class="h4 text-info mb-1">{{ task.progress_percentage }}%</div>
<small class="text-muted">Progress</small>
</div>
</div>
{% endif %}
<div class="col-6 col-md-3">
<div class="text-center">
<div class="h4 text-primary mb-1">{{ task.time_entries.count() }}</div>
<small class="text-muted">Time Entries</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h6>Estimated Hours</h6>
<p class="mb-3">
{% if task.estimated_hours %}
{{ task.estimated_hours }} hours
{% else %}
<span class="text-muted">Not estimated</span>
{% endif %}
</p>
</div>
<div class="col-md-6">
<h6>Actual Hours</h6>
<p class="mb-3">
{% if task.total_hours > 0 %}
{{ task.total_hours }} hours
{% else %}
<span class="text-muted">No time logged</span>
{% endif %}
</p>
</div>
</div>
{% if task.estimated_hours and task.total_hours > 0 %}
<div class="mb-3">
<h6>Progress</h6>
<div class="progress" style="height: 20px;">
<div class="progress-bar" role="progressbar"
<div class="mt-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<small class="text-muted">Progress Overview</small>
<small class="text-muted fw-bold">{{ task.progress_percentage }}% Complete</small>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-primary" role="progressbar"
style="width: {{ task.progress_percentage }}%"
aria-valuenow="{{ task.progress_percentage }}"
aria-valuemin="0" aria-valuemax="100">
{{ task.progress_percentage }}%
</div>
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-6">
<h6>Created</h6>
<p class="mb-3">{{ task.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
</div>
<div class="col-md-6">
<h6>Last Updated</h6>
<p class="mb-3">{{ task.updated_at.strftime('%Y-%m-%d %H:%M') }}</p>
</div>
</div>
{% if task.started_at %}
<div class="row">
<div class="col-md-6">
<h6>Started</h6>
<p class="mb-3">{{ task.started_at.strftime('%Y-%m-%d %H:%M') }}</p>
</div>
{% if task.completed_at %}
<div class="col-md-6">
<h6>Completed</h6>
<p class="mb-3">{{ task.completed_at.strftime('%Y-%m-%d %H:%M') }}</p>
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
<!-- Status Management -->
{% if task.is_active %}
<div class="card mb-4">
<!-- Recent Time Entries -->
{% if task.time_entries.count() > 0 %}
<div class="card mobile-card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-cogs"></i> Manage Status</h5>
<h6 class="mb-0">
<i class="fas fa-history me-2 text-info"></i>Recent Time Entries
</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('tasks.update_task_status', task_id=task.id) }}" class="d-inline">
<input type="hidden" name="status" value="in_progress">
<button type="submit" class="btn btn-warning me-2"
{% if task.status == 'in_progress' %}disabled{% endif %}>
<i class="fas fa-play"></i> Start Task
</button>
</form>
<form method="POST" action="{{ url_for('tasks.update_task_status', task_id=task.id) }}" class="d-inline">
<input type="hidden" name="status" value="review">
<button type="submit" class="btn btn-info me-2"
{% if task.status not in ['todo', 'in_progress'] %}disabled{% endif %}>
<i class="fas fa-eye"></i> Mark for Review
</button>
</form>
<form method="POST" action="{{ url_for('tasks.update_task_status', task_id=task.id) }}" class="d-inline">
<input type="hidden" name="status" value="done">
<button type="submit" class="btn btn-success me-2"
{% if task.status == 'done' %}disabled{% endif %}>
<i class="fas fa-check"></i> Complete Task
</button>
</form>
<form method="POST" action="{{ url_for('tasks.update_task_status', task_id=task.id) }}" class="d-inline">
<input type="hidden" name="status" value="cancelled">
<button type="submit" class="btn btn-secondary"
{% if task.status == 'cancelled' %}disabled{% endif %}>
<i class="fas fa-times"></i> Cancel Task
</button>
</form>
</div>
</div>
{% endif %}
<!-- Time Entries -->
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-clock"></i> Time Entries</h5>
</div>
<div class="card-body">
{% if time_entries %}
<div class="table-responsive">
<table class="table table-sm">
<table class="table table-hover">
<thead>
<tr>
<th>User</th>
<th>Start Time</th>
<th>End Time</th>
<th>Date</th>
<th>Duration</th>
<th>Notes</th>
<th>User</th>
</tr>
</thead>
<tbody>
{% for entry in time_entries %}
{% for entry in task.time_entries.order_by(desc('start_time')).limit(5).all() %}
<tr>
<td>{{ entry.user.username }}</td>
<td>{{ entry.start_time.strftime('%Y-%m-%d %H:%M') }}</td>
<td>{{ entry.start_time.strftime('%b %d, %Y') }}</td>
<td>
{% if entry.end_time %}
{{ entry.end_time.strftime('%Y-%m-%d %H:%M') }}
{% if entry.duration_seconds %}
{{ (entry.duration_seconds / 3600)|round(2) }}h
{% else %}
<span class="text-warning">Active</span>
{% endif %}
</td>
<td>{{ entry.duration_formatted }}</td>
<td>
{% if entry.notes %}
{{ entry.notes[:50] }}{% if entry.notes|length > 50 %}...{% endif %}
{% else %}
<span class="text-muted">No notes</span>
<span class="text-muted">-</span>
{% endif %}
</td>
<td>{{ entry.notes[:50] if entry.notes else '-' }}</td>
<td>{{ entry.user.username if entry.user else '-' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted text-center py-3">No time entries for this task yet.</p>
{% if task.time_entries.count() > 5 %}
<div class="text-center mt-3">
<small class="text-muted">Showing 5 of {{ task.time_entries.count() }} entries</small>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- Task Actions Sidebar -->
<div class="col-md-4">
<!-- Quick Actions -->
<div class="card mb-4">
<!-- Sidebar Information -->
<div class="col-lg-4">
<!-- Task Details -->
<div class="card mobile-card mb-4">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-bolt"></i> Quick Actions</h6>
<h6 class="mb-0">
<i class="fas fa-info-circle me-2 text-secondary"></i>Task Details
</h6>
</div>
<div class="card-body">
<div class="task-detail-item mb-3">
<small class="text-muted d-block mb-1">Status</small>
<span class="status-badge status-{{ task.status }}">
{{ task.status_display }}
</span>
</div>
<div class="task-detail-item mb-3">
<small class="text-muted d-block mb-1">Priority</small>
<span class="priority-badge priority-{{ task.priority }}">
{{ task.priority_display }}
</span>
</div>
{% if task.assigned_user %}
<div class="task-detail-item mb-3">
<small class="text-muted d-block mb-1">Assigned To</small>
<div class="d-flex align-items-center">
<div class="bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-user text-info fa-xs"></i>
</div>
<span>{{ task.assigned_user.username }}</span>
</div>
</div>
{% endif %}
<div class="task-detail-item mb-3">
<small class="text-muted d-block mb-1">Created By</small>
<div class="d-flex align-items-center">
<div class="bg-secondary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-user-plus text-secondary fa-xs"></i>
</div>
<span>{{ task.creator.username }}</span>
</div>
</div>
{% if task.due_date %}
<div class="task-detail-item mb-3">
<small class="text-muted d-block mb-1">Due Date</small>
<div class="d-flex align-items-center">
<div class="bg-{% if task.is_overdue %}danger{% else %}secondary{% endif %} bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-calendar text-{% if task.is_overdue %}danger{% else %}secondary{% endif %} fa-xs"></i>
</div>
<span class="{% if task.is_overdue %}text-danger fw-bold{% endif %}">
{{ task.due_date.strftime('%B %d, %Y') }}
</span>
</div>
</div>
{% endif %}
{% if task.started_at %}
<div class="task-detail-item mb-3">
<small class="text-muted d-block mb-1">Started</small>
<div class="d-flex align-items-center">
<div class="bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-play text-warning fa-xs"></i>
</div>
<span>{{ task.started_at.strftime('%B %d, %Y %H:%M') }}</span>
</div>
</div>
{% endif %}
{% if task.completed_at %}
<div class="task-detail-item mb-3">
<small class="text-muted d-block mb-1">Completed</small>
<div class="d-flex align-items-center">
<div class="bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-check text-success fa-xs"></i>
</div>
<span>{{ task.completed_at.strftime('%B %d, %Y %H:%M') }}</span>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Quick Actions -->
<div class="card mobile-card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-bolt me-2 text-warning"></i>Quick Actions
</h6>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{{ url_for('timer.start_timer', project_id=task.project.id, task_id=task.id) }}"
class="btn btn-success">
<i class="fas fa-play"></i> Start Timer
</a>
<a href="{{ url_for('timer.manual_entry', project_id=task.project.id, task_id=task.id) }}"
class="btn btn-outline-primary">
<i class="fas fa-plus"></i> Log Time
</a>
{% if current_user.is_admin or task.created_by == current_user.id %}
<a href="{{ url_for('tasks.edit_task', task_id=task.id) }}"
class="btn btn-outline-secondary">
<i class="fas fa-edit"></i> Edit Task
</a>
{% if task.status == 'todo' %}
<button class="btn btn-warning btn-sm" onclick="updateTaskStatus('in_progress')">
<i class="fas fa-play me-2"></i>Start Task
</button>
{% elif task.status == 'in_progress' %}
<button class="btn btn-info btn-sm" onclick="updateTaskStatus('review')">
<i class="fas fa-eye me-2"></i>Mark for Review
</button>
<button class="btn btn-secondary btn-sm" onclick="updateTaskStatus('todo')">
<i class="fas fa-pause me-2"></i>Pause Task
</button>
{% elif task.status == 'review' %}
<button class="btn btn-success btn-sm" onclick="updateTaskStatus('done')">
<i class="fas fa-check me-2"></i>Complete Task
</button>
<button class="btn btn-warning btn-sm" onclick="updateTaskStatus('in_progress')">
<i class="fas fa-undo me-2"></i>Back to Progress
</button>
{% endif %}
{% if task.status != 'done' and task.status != 'cancelled' %}
<button class="btn btn-danger btn-sm" onclick="updateTaskStatus('cancelled')">
<i class="fas fa-times me-2"></i>Cancel Task
</button>
{% endif %}
</div>
</div>
</div>
<!-- Task Statistics -->
<div class="card mb-4">
<!-- Task Timeline -->
<div class="card mobile-card">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-chart-bar"></i> Statistics</h6>
<h6 class="mb-0">
<i class="fas fa-history me-2 text-info"></i>Task Timeline
</h6>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-6">
<div class="border-end">
<h4 class="text-primary mb-1">{{ task.total_hours }}</h4>
<small class="text-muted">Hours Logged</small>
<div class="timeline">
<div class="timeline-item">
<div class="timeline-marker bg-primary"></div>
<div class="timeline-content">
<small class="text-muted">{{ task.created_at.strftime('%b %d, %Y') }}</small>
<p class="mb-0">Task created</p>
</div>
</div>
<div class="col-6">
<h4 class="text-success mb-1">{{ time_entries|length }}</h4>
<small class="text-muted">Time Entries</small>
</div>
</div>
{% if task.estimated_hours %}
<hr>
<div class="text-center">
<h6>Estimated vs Actual</h6>
<div class="progress mb-2" style="height: 8px;">
<div class="progress-bar" role="progressbar"
style="width: {{ task.progress_percentage }}%">
{% if task.started_at %}
<div class="timeline-item">
<div class="timeline-marker bg-warning"></div>
<div class="timeline-content">
<small class="text-muted">{{ task.started_at.strftime('%b %d, %Y') }}</small>
<p class="mb-0">Task started</p>
</div>
</div>
{% endif %}
{% if task.completed_at %}
<div class="timeline-item">
<div class="timeline-marker bg-success"></div>
<div class="timeline-content">
<small class="text-muted">{{ task.completed_at.strftime('%b %d, %Y') }}</small>
<p class="mb-0">Task completed</p>
</div>
</div>
{% endif %}
<div class="timeline-item">
<div class="timeline-marker bg-secondary"></div>
<div class="timeline-content">
<small class="text-muted">{{ task.updated_at.strftime('%b %d, %Y') }}</small>
<p class="mb-0">Last updated</p>
</div>
</div>
<small class="text-muted">
{{ task.progress_percentage }}% of estimate
</small>
</div>
{% endif %}
</div>
</div>
<!-- Delete Task -->
{% if current_user.is_admin or task.created_by == current_user.id %}
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h6 class="mb-0"><i class="fas fa-trash"></i> Danger Zone</h6>
</div>
<div class="card-body">
<p class="text-muted small mb-3">
Deleting a task will permanently remove it and all associated data.
</p>
<form method="POST" action="{{ url_for('tasks.delete_task', task_id=task.id) }}"
onsubmit="return confirm('Are you sure you want to delete this task? This action cannot be undone.')">
<button type="submit" class="btn btn-danger btn-sm w-100">
<i class="fas fa-trash"></i> Delete Task
</button>
</form>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<style>
.priority-badge {
font-size: 0.875rem;
/* Status and Priority Badges */
.status-badge, .priority-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-todo {
background-color: #e2e8f0;
color: #475569;
}
.status-in_progress {
background-color: #fef3c7;
color: #92400e;
}
.status-review {
background-color: #dbeafe;
color: #1e40af;
}
.status-done {
background-color: #dcfce7;
color: #166534;
}
.status-cancelled {
background-color: #fee2e2;
color: #991b1b;
}
.priority-low {
background-color: #28a745 !important;
background-color: #dcfce7;
color: #166534;
}
.priority-medium {
background-color: #ffc107 !important;
color: #212529 !important;
background-color: #fef3c7;
color: #92400e;
}
.priority-high {
background-color: #fd7e14 !important;
background-color: #fed7aa;
color: #c2410c;
}
.priority-urgent {
background-color: #dc3545 !important;
background-color: #fee2e2;
color: #991b1b;
}
/* Task Detail Items */
.task-detail-item {
padding: 0.75rem;
border-radius: 8px;
background-color: #f8fafc;
transition: all 0.2s ease;
}
.task-detail-item:hover {
background-color: #f1f5f9;
transform: translateX(4px);
}
/* Timeline Styling */
.timeline {
position: relative;
padding-left: 1rem;
}
.timeline::before {
content: '';
position: absolute;
left: 0.5rem;
top: 0;
bottom: 0;
width: 2px;
background-color: #e2e8f0;
}
.timeline-item {
position: relative;
margin-bottom: 1.5rem;
}
.timeline-marker {
position: absolute;
left: -0.75rem;
top: 0.25rem;
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 0 0 2px #e2e8f0;
}
.timeline-content {
margin-left: 1rem;
}
.timeline-content small {
font-size: 0.75rem;
}
.timeline-content p {
font-size: 0.875rem;
margin: 0.25rem 0 0 0;
}
/* Progress Bar */
.progress {
background-color: #e9ecef;
background-color: #f1f5f9;
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
background-color: #007bff;
}
.border-end {
border-right: 1px solid #dee2e6;
background: linear-gradient(90deg, var(--primary-color) 0%, var(--primary-dark) 100%);
border-radius: 10px;
}
/* Mobile Optimizations */
@media (max-width: 768px) {
.border-end {
border-right: none !important;
border-bottom: 1px solid #dee2e6;
padding-bottom: 1rem;
margin-bottom: 1rem;
.card-header {
padding: 1rem 1rem 0.75rem 1rem;
}
.card-body {
padding: 0.75rem 1rem;
}
.timeline {
padding-left: 0.75rem;
}
.timeline-marker {
left: -0.625rem;
width: 10px;
height: 10px;
}
.timeline-content {
margin-left: 0.75rem;
}
}
/* Hover Effects */
.task-detail-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
</style>
<script>
function updateTaskStatus(status) {
if (confirm('Are you sure you want to update the task status?')) {
// Simple form submission approach instead of fetch with CSRF token
const form = document.createElement('form');
form.method = 'POST';
form.action = '{{ url_for("tasks.update_task_status", task_id=task.id) }}';
const statusInput = document.createElement('input');
statusInput.type = 'hidden';
statusInput.name = 'status';
statusInput.value = status;
form.appendChild(statusInput);
document.body.appendChild(form);
form.submit();
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,53 @@
This is a placeholder for the enhanced task management screenshot.
The screenshot should show:
1. **Modern Header Section**
- Card-based header with task icon and description
- Clean, professional appearance
2. **Quick Stats Section**
- Cards showing task counts by status (To Do, In Progress, Review, Done)
- Color-coded status indicators
- Modern card design with hover effects
3. **Enhanced Filter Section**
- Search input with icon
- Dropdown filters for status, priority, project, assignee
- Apply Filters and Clear buttons
- Professional form layout
4. **Task Cards Grid**
- Responsive grid layout of task cards
- Each card showing:
- Task name and description
- Priority badge with color coding
- Status badge
- Project information
- Due date with overdue indicators
- Assigned user
- Estimated vs actual hours
- Quick action buttons
- Hover effects and smooth transitions
- Priority-based left border colors
5. **Mobile-Responsive Design**
- Cards that stack properly on mobile
- Touch-friendly buttons and interactions
- Optimized spacing for all screen sizes
6. **Modern UI Elements**
- Bootstrap 5 styling
- Custom CSS variables for consistent theming
- Smooth animations and transitions
- Professional color scheme
- Icon integration (Font Awesome)
The overall appearance should be:
- Clean and modern
- Professional and business-like
- Easy to read and navigate
- Visually appealing with good contrast
- Consistent with the application's design system
This screenshot will replace the existing Tasks.png to showcase the new enhanced interface.

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

58
docker/fix-schema.py Normal file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""
Simple script to fix the missing task_id column
"""
import os
import sys
import time
from sqlalchemy import create_engine, text, inspect
def fix_schema():
"""Fix the missing task_id column"""
url = os.getenv("DATABASE_URL", "")
if not url.startswith("postgresql"):
print("No PostgreSQL database configured")
return False
try:
engine = create_engine(url, pool_pre_ping=True)
inspector = inspect(engine)
# Check if time_entries table exists
if 'time_entries' not in inspector.get_table_names():
print("time_entries table not found")
return False
# Check if task_id column exists
columns = inspector.get_columns("time_entries")
column_names = [col['name'] for col in columns]
print(f"Current columns in time_entries: {column_names}")
if 'task_id' in column_names:
print("task_id column already exists")
return True
# Add the missing column
print("Adding task_id column...")
with engine.connect() as conn:
conn.execute(text("ALTER TABLE time_entries ADD COLUMN task_id INTEGER;"))
conn.commit()
print("✓ task_id column added successfully")
return True
except Exception as e:
print(f"Error fixing schema: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
if fix_schema():
print("Schema fix completed successfully")
sys.exit(0)
else:
print("Schema fix failed")
sys.exit(1)

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""
Force schema update script for TimeTracker
This script forces the addition of missing columns to existing tables
"""
import os
import sys
import time
from sqlalchemy import create_engine, text, inspect
def wait_for_database(url, max_attempts=30, delay=2):
"""Wait for database to be ready"""
print(f"Waiting for database to be ready...")
for attempt in range(max_attempts):
try:
engine = create_engine(url, pool_pre_ping=True)
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
print("Database connection established successfully")
return engine
except Exception as e:
print(f"Waiting for database... (attempt {attempt+1}/{max_attempts}): {e}")
if attempt < max_attempts - 1:
time.sleep(delay)
else:
print("Database not ready after waiting, exiting...")
sys.exit(1)
return None
def force_schema_update(engine):
"""Force update the database schema"""
print("Forcing schema update...")
try:
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
# Check if tasks table exists
if 'tasks' not in existing_tables:
print("Creating tasks table...")
create_tasks_sql = """
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
priority VARCHAR(20) DEFAULT 'medium' NOT NULL,
assigned_to INTEGER REFERENCES users(id),
created_by INTEGER REFERENCES users(id) NOT NULL,
due_date DATE,
estimated_hours NUMERIC(5,2),
actual_hours NUMERIC(5,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
"""
with engine.connect() as conn:
conn.execute(text(create_tasks_sql))
conn.commit()
print("✓ Tasks table created successfully")
else:
print("✓ Tasks table already exists")
# Check if time_entries table exists and has task_id column
if 'time_entries' in existing_tables:
time_entries_columns = [col['name'] for col in inspector.get_columns('time_entries')]
if 'task_id' not in time_entries_columns:
print("Adding task_id column to time_entries table...")
add_column_sql = """
ALTER TABLE time_entries
ADD COLUMN task_id INTEGER;
"""
with engine.connect() as conn:
conn.execute(text(add_column_sql))
conn.commit()
print("✓ task_id column added to time_entries table")
else:
print("✓ task_id column already exists in time_entries table")
else:
print("⚠ Warning: time_entries table does not exist")
print("✓ Schema update completed successfully")
return True
except Exception as e:
print(f"✗ Error updating schema: {e}")
import traceback
traceback.print_exc()
return False
def main():
"""Main function"""
url = os.getenv("DATABASE_URL", "")
if not url.startswith("postgresql"):
print("No PostgreSQL database configured, skipping schema update")
return
print(f"Database URL: {url}")
# Wait for database to be ready
engine = wait_for_database(url)
# Force schema update
if force_schema_update(engine):
print("Schema update completed successfully")
sys.exit(0)
else:
print("Schema update failed")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""
Simple database initialization script for TimeTracker
This script ensures the database has the correct schema
"""
import os
import sys
import time
from sqlalchemy import create_engine, text, inspect
def wait_for_database(url, max_attempts=30, delay=2):
"""Wait for database to be ready"""
print(f"Waiting for database to be ready...")
for attempt in range(max_attempts):
try:
engine = create_engine(url, pool_pre_ping=True)
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
print("Database connection established successfully")
return engine
except Exception as e:
print(f"Waiting for database... (attempt {attempt+1}/{max_attempts}): {e}")
if attempt < max_attempts - 1:
time.sleep(delay)
else:
print("Database not ready after waiting, exiting...")
sys.exit(1)
return None
def ensure_correct_schema(engine):
"""Ensure the database has the correct schema"""
print("Ensuring correct database schema...")
try:
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
# Check if tasks table exists
if 'tasks' not in existing_tables:
print("Creating tasks table...")
create_tasks_sql = """
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
priority VARCHAR(20) DEFAULT 'medium' NOT NULL,
assigned_to INTEGER REFERENCES users(id),
created_by INTEGER REFERENCES users(id) NOT NULL,
due_date DATE,
estimated_hours NUMERIC(5,2),
actual_hours NUMERIC(5,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
"""
with engine.connect() as conn:
conn.execute(text(create_tasks_sql))
conn.commit()
print("✓ Tasks table created successfully")
else:
print("✓ Tasks table already exists")
# Check if time_entries table exists and has task_id column
if 'time_entries' in existing_tables:
time_entries_columns = [col['name'] for col in inspector.get_columns('time_entries')]
if 'task_id' not in time_entries_columns:
print("Adding task_id column to time_entries table...")
add_column_sql = """
ALTER TABLE time_entries
ADD COLUMN task_id INTEGER;
"""
with engine.connect() as conn:
conn.execute(text(add_column_sql))
conn.commit()
print("✓ task_id column added to time_entries table")
else:
print("✓ task_id column already exists in time_entries table")
else:
print("⚠ Warning: time_entries table does not exist")
print("✓ Database schema is correct")
return True
except Exception as e:
print(f"✗ Error ensuring correct schema: {e}")
import traceback
traceback.print_exc()
return False
def main():
"""Main function"""
url = os.getenv("DATABASE_URL", "")
if not url.startswith("postgresql"):
print("No PostgreSQL database configured, skipping initialization")
return
print(f"Database URL: {url}")
# Wait for database to be ready
engine = wait_for_database(url)
# Ensure correct schema
if ensure_correct_schema(engine):
print("Database schema verification completed successfully")
sys.exit(0)
else:
print("Database schema verification failed")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -66,6 +66,7 @@ def create_tables_sql(engine):
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
task_id INTEGER,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP,
duration_seconds INTEGER,

View File

@@ -34,7 +34,7 @@ def wait_for_database(url, max_attempts=30, delay=2):
return None
def check_database_initialization(engine):
"""Check if database is initialized by looking for required tables"""
"""Check if database is initialized by looking for required tables and correct schema"""
print("Checking if database is initialized...")
try:
@@ -50,7 +50,29 @@ def check_database_initialization(engine):
print(f"Database not fully initialized. Missing tables: {missing_tables}")
return False
else:
print("Database is already initialized with all required tables")
print("✓ All required tables exist")
# Check if tables have the correct schema
print("Checking table schemas...")
# Check if time_entries has task_id column
if 'time_entries' in existing_tables:
time_entries_columns = [col['name'] for col in inspector.get_columns('time_entries')]
print(f"Debug: time_entries columns found: {time_entries_columns}")
if 'task_id' not in time_entries_columns:
print(f"✗ time_entries table missing task_id column. Available columns: {time_entries_columns}")
return False
else:
print("✓ time_entries table has correct schema")
# Check if tasks table exists
if 'tasks' not in existing_tables:
print("✗ tasks table missing")
return False
else:
print("✓ tasks table exists")
print("✓ Database is already initialized with all required tables and correct schema")
return True
except Exception as e:
@@ -58,6 +80,48 @@ def check_database_initialization(engine):
print(f"Traceback: {traceback.format_exc()}")
return False
def check_table_schema(engine, table_name, required_columns):
"""Check if a table has the required columns"""
try:
inspector = inspect(engine)
if table_name not in inspector.get_table_names():
return False
existing_columns = [col['name'] for col in inspector.get_columns(table_name)]
missing_columns = [col for col in required_columns if col not in existing_columns]
if missing_columns:
print(f"Table {table_name} missing columns: {missing_columns}")
return False
return True
except Exception as e:
print(f"Error checking schema for {table_name}: {e}")
return False
def ensure_correct_schema(engine):
"""Ensure all tables have the correct schema"""
print("Checking table schemas...")
# Define required columns for each table
required_columns = {
'time_entries': ['id', 'user_id', 'project_id', 'task_id', 'start_time', 'end_time',
'duration_seconds', 'notes', 'tags', 'source', 'billable', 'created_at', 'updated_at'],
'tasks': ['id', 'project_id', 'name', 'description', 'status', 'priority', 'assigned_to',
'created_by', 'due_date', 'estimated_hours', 'actual_hours', 'created_at', 'updated_at']
}
needs_recreation = False
for table_name, columns in required_columns.items():
if not check_table_schema(engine, table_name, columns):
print(f"Table {table_name} needs recreation")
needs_recreation = True
return needs_recreation
def initialize_database(engine):
"""Initialize database using Flask CLI command"""
print("Initializing database...")
@@ -78,6 +142,12 @@ def initialize_database(engine):
print("Setting up app context...")
with app.app_context():
# Check if we need to recreate tables due to schema mismatch
if ensure_correct_schema(engine):
print("Schema mismatch detected, dropping and recreating tables...")
db.drop_all()
print("All tables dropped")
print("Creating all tables...")
# Create all tables
db.create_all()
@@ -155,10 +225,13 @@ def main():
engine = wait_for_database(url)
# Check if database is initialized
print("=== Starting database initialization check ===")
if not check_database_initialization(engine):
print("=== Database not initialized, starting initialization ===")
# Initialize database
if initialize_database(engine):
print("Database initialization completed successfully")
# Verify initialization worked
if check_database_initialization(engine):
print("Database verification successful")
@@ -169,7 +242,26 @@ def main():
print("Database initialization failed")
sys.exit(1)
else:
print("Database already initialized, no action needed")
print("=== Database already initialized, checking if reinitialization is needed ===")
# Even if database is initialized, double-check schema and reinitialize if needed
print("Double-checking schema for existing database...")
if ensure_correct_schema(engine):
print("Schema mismatch detected in existing database, reinitializing...")
if initialize_database(engine):
print("Database reinitialization completed successfully")
# Verify reinitialization worked
if check_database_initialization(engine):
print("Database verification successful after reinitialization")
else:
print("Database verification failed after reinitialization")
sys.exit(1)
else:
print("Database reinitialization failed")
sys.exit(1)
else:
print("Schema is correct, no reinitialization needed")
if __name__ == "__main__":
main()

View File

@@ -82,7 +82,7 @@ def migrate_task_management(engine):
# Add task_id column
add_column_sql = """
ALTER TABLE time_entries
ADD COLUMN task_id INTEGER REFERENCES tasks(id);
ADD COLUMN task_id INTEGER;
"""
# Create index for performance
@@ -97,6 +97,40 @@ def migrate_task_management(engine):
print("✓ task_id column added to time_entries table")
else:
print("✓ task_id column already exists in time_entries table")
# Add foreign key constraint if it doesn't exist
try:
# Check if foreign key constraint exists
constraints_sql = """
SELECT constraint_name
FROM information_schema.table_constraints
WHERE table_name = 'time_entries'
AND constraint_type = 'FOREIGN KEY'
AND constraint_name LIKE '%task_id%';
"""
with engine.connect() as conn:
result = conn.execute(text(constraints_sql))
constraints = [row[0] for row in result]
if not constraints:
print("Adding foreign key constraint for task_id...")
add_fk_sql = """
ALTER TABLE time_entries
ADD CONSTRAINT fk_time_entries_task_id
FOREIGN KEY (task_id) REFERENCES tasks(id);
"""
with engine.connect() as conn:
conn.execute(text(add_fk_sql))
conn.commit()
print("✓ Foreign key constraint added for task_id")
else:
print("✓ Foreign key constraint already exists for task_id")
except Exception as e:
print(f"Warning: Could not add foreign key constraint: {e}")
print("This is not critical, continuing...")
else:
print("⚠ Warning: time_entries table does not exist")

184
docker/start-fixed.sh Normal file
View File

@@ -0,0 +1,184 @@
#!/bin/bash
set -e
cd /app
export FLASK_APP=app
echo "=== Starting TimeTracker ==="
echo "Waiting for database to be ready..."
# Wait for Postgres to be ready
python - <<"PY"
import os
import time
import sys
from sqlalchemy import create_engine, text
from sqlalchemy.exc import OperationalError
url = os.getenv("DATABASE_URL", "")
if url.startswith("postgresql"):
for attempt in range(30):
try:
engine = create_engine(url, pool_pre_ping=True)
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
print("Database connection established successfully")
break
except Exception as e:
print(f"Waiting for database... (attempt {attempt+1}/30): {e}")
time.sleep(2)
else:
print("Database not ready after waiting, exiting...")
sys.exit(1)
else:
print("No PostgreSQL database configured, skipping connection check")
PY
echo "=== FIXING DATABASE SCHEMA ==="
# Step 1: Create tasks table if it doesn't exist
echo "Step 1: Ensuring tasks table exists..."
python - <<"PY"
import os
import sys
from sqlalchemy import create_engine, text, inspect
url = os.getenv("DATABASE_URL", "")
if url.startswith("postgresql"):
try:
engine = create_engine(url, pool_pre_ping=True)
inspector = inspect(engine)
if 'tasks' not in inspector.get_table_names():
print("Creating tasks table...")
create_tasks_sql = """
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
priority VARCHAR(20) DEFAULT 'medium' NOT NULL,
assigned_to INTEGER REFERENCES users(id),
created_by INTEGER REFERENCES users(id) NOT NULL,
due_date DATE,
estimated_hours NUMERIC(5,2),
actual_hours NUMERIC(5,2),
started_at TIMESTAMP,
completed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);
"""
with engine.connect() as conn:
conn.execute(text(create_tasks_sql))
conn.commit()
print("✓ Tasks table created successfully")
else:
print("✓ Tasks table already exists")
except Exception as e:
print(f"Error creating tasks table: {e}")
sys.exit(1)
else:
print("No PostgreSQL database configured")
sys.exit(0)
PY
# Step 2: Add task_id column to time_entries if it doesn't exist
echo "Step 2: Ensuring task_id column exists in time_entries..."
python - <<"PY"
import os
import sys
from sqlalchemy import create_engine, text, inspect
url = os.getenv("DATABASE_URL", "")
if url.startswith("postgresql"):
try:
engine = create_engine(url, pool_pre_ping=True)
inspector = inspect(engine)
if 'time_entries' in inspector.get_table_names():
columns = inspector.get_columns("time_entries")
column_names = [col['name'] for col in columns]
print(f"Current columns in time_entries: {column_names}")
if 'task_id' not in column_names:
print("Adding task_id column...")
with engine.connect() as conn:
conn.execute(text("ALTER TABLE time_entries ADD COLUMN task_id INTEGER;"))
conn.commit()
print("✓ task_id column added successfully")
else:
print("✓ task_id column already exists")
else:
print("⚠ Warning: time_entries table does not exist")
except Exception as e:
print(f"Error adding task_id column: {e}")
sys.exit(1)
else:
print("No PostgreSQL database configured")
sys.exit(0)
PY
# Step 2.5: Add missing columns to tasks table if it exists
echo "Step 2.5: Ensuring tasks table has all required columns..."
python - <<"PY"
import os
import sys
from sqlalchemy import create_engine, text, inspect
url = os.getenv("DATABASE_URL", "")
if url.startswith("postgresql"):
try:
engine = create_engine(url, pool_pre_ping=True)
inspector = inspect(engine)
if 'tasks' in inspector.get_table_names():
columns = inspector.get_columns("tasks")
column_names = [col['name'] for col in columns]
print(f"Current columns in tasks: {column_names}")
# Check for missing columns
missing_columns = []
required_columns = ['started_at', 'completed_at']
for col in required_columns:
if col not in column_names:
missing_columns.append(col)
if missing_columns:
print(f"Adding missing columns to tasks table: {missing_columns}")
with engine.connect() as conn:
for col in missing_columns:
if col == 'started_at':
conn.execute(text("ALTER TABLE tasks ADD COLUMN started_at TIMESTAMP;"))
elif col == 'completed_at':
conn.execute(text("ALTER TABLE tasks ADD COLUMN completed_at TIMESTAMP;"))
conn.commit()
print("✓ Missing columns added to tasks table successfully")
else:
print("✓ tasks table has all required columns")
else:
print("⚠ Warning: tasks table does not exist")
except Exception as e:
print(f"Error adding missing columns to tasks: {e}")
sys.exit(1)
else:
print("No PostgreSQL database configured")
sys.exit(0)
PY
# Step 3: Run the main database initialization
echo "Step 3: Running main database initialization..."
python /app/docker/init-database.py
if [ $? -ne 0 ]; then
echo "Database initialization failed. Exiting."
exit 1
fi
echo "=== DATABASE SCHEMA FIXED SUCCESSFULLY ==="
echo "Starting application..."
exec gunicorn --bind 0.0.0.0:8080 --worker-class eventlet --workers 1 --timeout 120 "app:create_app()"

View File

@@ -34,160 +34,23 @@ else:
PY
echo "Checking if database is initialized..."
# Check if database is initialized by looking for tables
python - <<"PY"
import os
import sys
from sqlalchemy import create_engine, text, inspect
url = os.getenv("DATABASE_URL", "")
if url.startswith("postgresql"):
try:
engine = create_engine(url, pool_pre_ping=True)
inspector = inspect(engine)
# Check if our main tables exist
existing_tables = inspector.get_table_names()
required_tables = ["users", "projects", "time_entries", "settings", "tasks"]
missing_tables = [table for table in required_tables if table not in existing_tables]
if missing_tables:
print(f"Database not fully initialized. Missing tables: {missing_tables}")
print("Will initialize database...")
sys.exit(1) # Exit with error to trigger initialization
else:
print("Database is already initialized with all required tables")
# Check if Task Management migration is needed
try:
# Check if tasks table exists
if 'tasks' not in existing_tables:
print("Task Management tables missing, will initialize...")
sys.exit(1) # Exit to trigger initialization
# Check if task_id column exists in time_entries table
columns = inspector.get_columns("time_entries")
column_names = [col['name'] for col in columns]
if 'task_id' not in column_names:
print("Task Management columns missing, will run migration...")
sys.exit(3) # Special exit code for Task Management migration
# Check if migration is needed for field names
has_old_fields = 'start_utc' in column_names or 'end_utc' in column_names
if has_old_fields:
print("Migration needed for field names")
sys.exit(2) # Special exit code for migration
else:
print("No migration needed")
sys.exit(0) # Exit successfully, no initialization needed
except Exception as e:
print(f"Error checking migration status: {e}")
sys.exit(0) # Assume no migration needed
except Exception as e:
print(f"Error checking database initialization: {e}")
sys.exit(1)
else:
print("No PostgreSQL database configured, skipping initialization check")
sys.exit(0)
PY
if [ $? -eq 1 ]; then
echo "Initializing database with Python-based script..."
python /app/docker/init-database.py
if [ $? -ne 0 ]; then
echo "Database initialization failed. Exiting to prevent infinite loop."
exit 1
fi
echo "Database initialized successfully"
# Run migration if needed
echo "Running database migration..."
python /app/docker/migrate-field-names.py
# Run Task Management migration if needed
echo "Running Task Management migration..."
python /app/docker/migrate-add-task-columns.py
# Verify initialization worked
echo "Verifying database initialization..."
elif [ $? -eq 2 ]; then
echo "Running database migration for existing database..."
python /app/docker/migrate-field-names.py
if [ $? -ne 0 ]; then
echo "Database migration failed. Exiting."
exit 1
fi
echo "Database migration completed successfully"
# Also run Task Management migration for existing databases
echo "Running Task Management migration for existing database..."
python /app/docker/migrate-add-task-columns.py
if [ $? -ne 0 ]; then
echo "Task Management migration failed. Exiting."
exit 1
fi
echo "Task Management migration completed successfully"
elif [ $? -eq 3 ]; then
echo "Running Task Management migration for existing database..."
python /app/docker/migrate-add-task-columns.py
if [ $? -ne 0 ]; then
echo "Task Management migration failed. Exiting."
exit 1
fi
echo "Task Management migration completed successfully"
# Verify the migration worked
echo "Verifying Task Management migration..."
python - <<"PY"
import os
import sys
from sqlalchemy import create_engine, text, inspect
url = os.getenv("DATABASE_URL", "")
if url.startswith("postgresql"):
try:
engine = create_engine(url, pool_pre_ping=True)
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
required_tables = ["users", "projects", "time_entries", "settings", "tasks"]
missing_tables = [table for table in required_tables if table not in existing_tables]
if missing_tables:
print(f"Database verification failed. Missing tables: {missing_tables}")
sys.exit(1)
# Check if task_id column exists in time_entries table
try:
columns = inspector.get_columns("time_entries")
column_names = [col['name'] for col in columns]
if 'task_id' not in column_names:
print("Database verification failed. Missing task_id column in time_entries table")
sys.exit(1)
except Exception as e:
print(f"Error checking time_entries columns: {e}")
sys.exit(1)
print("Database verification successful")
sys.exit(0)
except Exception as e:
print(f"Error verifying database: {e}")
sys.exit(1)
else:
print("No PostgreSQL database configured, skipping verification")
sys.exit(0)
PY
if [ $? -eq 1 ]; then
echo "Database verification failed after initialization. Exiting to prevent infinite loop."
exit 1
fi
else
echo "Database already initialized, skipping initialization"
# Always run the database initialization script to ensure proper schema
echo "Running database initialization script..."
python /app/docker/init-database.py
if [ $? -ne 0 ]; then
echo "Database initialization failed. Exiting to prevent infinite loop."
exit 1
fi
echo "Database initialization completed successfully"
# Also run the simple schema fix to ensure task_id column exists
echo "Running schema fix script..."
python /app/docker/fix-schema.py
if [ $? -ne 0 ]; then
echo "Schema fix failed. Exiting."
exit 1
fi
echo "Schema fix completed successfully"
echo "Starting application..."
exec gunicorn --bind 0.0.0.0:8080 --worker-class eventlet --workers 1 --timeout 120 "app:create_app()"

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""
Test script to verify database schema is correct
"""
import os
import sys
from sqlalchemy import create_engine, text, inspect
def test_schema():
"""Test if the database has the correct schema"""
url = os.getenv("DATABASE_URL", "")
if not url.startswith("postgresql"):
print("No PostgreSQL database configured")
return False
try:
engine = create_engine(url, pool_pre_ping=True)
inspector = inspect(engine)
# Check required tables
existing_tables = inspector.get_table_names()
required_tables = ["users", "projects", "time_entries", "settings", "tasks"]
missing_tables = [table for table in required_tables if table not in existing_tables]
if missing_tables:
print(f"Missing tables: {missing_tables}")
return False
print("✓ All required tables exist")
# Check time_entries has task_id column
if 'time_entries' in existing_tables:
columns = inspector.get_columns("time_entries")
column_names = [col['name'] for col in columns]
print(f"time_entries columns: {column_names}")
if 'task_id' not in column_names:
print("✗ time_entries table missing task_id column")
return False
else:
print("✓ time_entries table has task_id column")
# Check tasks table structure
if 'tasks' in existing_tables:
columns = inspector.get_columns("tasks")
column_names = [col['name'] for col in columns]
print(f"tasks columns: {column_names}")
print("✓ tasks table exists with correct structure")
print("✓ Database schema is correct")
return True
except Exception as e:
print(f"Error testing schema: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
if test_schema():
print("Schema test PASSED")
sys.exit(0)
else:
print("Schema test FAILED")
sys.exit(1)

61
docker/test-schema.py Normal file
View File

@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
Test script to verify database schema
"""
import os
import sys
from sqlalchemy import create_engine, text, inspect
def test_schema():
"""Test if the database has the correct schema"""
url = os.getenv("DATABASE_URL", "")
if not url.startswith("postgresql"):
print("No PostgreSQL database configured")
return False
try:
engine = create_engine(url, pool_pre_ping=True)
inspector = inspect(engine)
# Check if required tables exist
existing_tables = inspector.get_table_names()
required_tables = ["users", "projects", "time_entries", "settings", "tasks"]
missing_tables = [table for table in required_tables if table not in existing_tables]
if missing_tables:
print(f"Missing tables: {missing_tables}")
return False
# Check if time_entries has task_id column
if 'time_entries' in existing_tables:
columns = inspector.get_columns("time_entries")
column_names = [col['name'] for col in columns]
if 'task_id' not in column_names:
print("time_entries table missing task_id column")
print(f"Available columns: {column_names}")
return False
else:
print("✓ time_entries table has task_id column")
else:
print("time_entries table not found")
return False
print("✓ Database schema is correct")
return True
except Exception as e:
print(f"Error testing schema: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
if test_schema():
print("Schema test passed")
sys.exit(0)
else:
print("Schema test failed")
sys.exit(1)