Merge pull request #28 from DRYTRIX/Bug-TaskViewDropdownNotCorrect

fix(tasks,ui): correct Task View dropdown behavior and unify filter UI
This commit is contained in:
Dries Peeters
2025-09-03 14:18:24 +02:00
committed by GitHub
14 changed files with 1302 additions and 660 deletions
+58 -12
View File
@@ -332,22 +332,68 @@ def delete_task(task_id):
@tasks_bp.route('/tasks/my-tasks')
@login_required
def my_tasks():
"""Show current user's tasks"""
"""Show current user's tasks with filters and pagination"""
page = request.args.get('page', 1, type=int)
status = request.args.get('status', '')
query = Task.query.filter(
db.or_(
Task.assigned_to == current_user.id,
Task.created_by == current_user.id
priority = request.args.get('priority', '')
project_id = request.args.get('project_id', type=int)
search = request.args.get('search', '').strip()
task_type = request.args.get('task_type', '') # '', 'assigned', 'created'
query = Task.query
# Restrict to current user's tasks depending on task_type filter
if task_type == 'assigned':
query = query.filter(Task.assigned_to == current_user.id)
elif task_type == 'created':
query = query.filter(Task.created_by == current_user.id)
else:
query = query.filter(
db.or_(
Task.assigned_to == current_user.id,
Task.created_by == current_user.id
)
)
)
# Apply filters
if status:
query = query.filter_by(status=status)
tasks = query.order_by(Task.priority.desc(), Task.due_date.asc(), Task.created_at.asc()).all()
return render_template('tasks/my_tasks.html', tasks=tasks, status=status)
if priority:
query = query.filter_by(priority=priority)
if project_id:
query = query.filter_by(project_id=project_id)
if search:
like = f"%{search}%"
query = query.filter(
db.or_(
Task.name.ilike(like),
Task.description.ilike(like)
)
)
tasks = query.order_by(
Task.priority.desc(),
Task.due_date.asc(),
Task.created_at.asc()
).paginate(page=page, per_page=20, error_out=False)
# Provide projects for filter dropdown
projects = Project.query.filter_by(status='active').order_by(Project.name).all()
return render_template(
'tasks/my_tasks.html',
tasks=tasks.items,
pagination=tasks,
projects=projects,
status=status,
priority=priority,
project_id=project_id,
search=search,
task_type=task_type
)
@tasks_bp.route('/tasks/overdue')
@login_required
+108 -4
View File
@@ -77,7 +77,8 @@ main {
border-radius: var(--border-radius);
transition: var(--transition);
background: white;
overflow: hidden;
/* Allow dropdown menus within cards to overflow properly */
overflow: visible;
margin-bottom: var(--card-spacing);
}
@@ -85,7 +86,7 @@ main {
margin-bottom: 0;
}
.card:hover {
.card.hover-lift:hover {
box-shadow: var(--card-shadow-hover);
transform: translateY(-2px);
}
@@ -256,6 +257,16 @@ main {
color: var(--text-primary);
}
/* Keep outline secondary buttons light when opened/active */
.btn-outline-secondary:focus,
.btn-outline-secondary:active,
.btn-outline-secondary.dropdown-toggle.show,
.show > .btn-outline-secondary.dropdown-toggle {
background: var(--light-color);
border-color: var(--text-secondary);
color: var(--text-primary);
}
/* Unify small/large sizes */
.btn-sm {
padding: 0.4rem 0.65rem;
@@ -705,11 +716,55 @@ h6 { font-size: 1rem; }
background: #ffffff !important;
-webkit-backdrop-filter: none !important;
backdrop-filter: none !important;
z-index: 1055; /* above navbar (1030) */
z-index: 1060; /* above navbar (1030) and our backdrop */
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
box-shadow: var(--card-shadow-hover);
margin-top: 0.5rem;
overflow: visible; /* allow soft shadow rounding */
position: absolute !important; /* ensure above backdrop and positioned by Bootstrap */
pointer-events: auto; /* capture interactions */
background-clip: padding-box; /* ensure solid fill to rounded corners */
}
/* Ensure dropdowns inside cards stack above adjacent content */
.card .dropdown,
.mobile-card .dropdown {
position: relative;
z-index: 2000;
}
.dropdown-item {
background-color: transparent;
background-color: #ffffff !important; /* make items opaque */
}
.dropdown-menu::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #ffffff; /* solid background to avoid transparency */
border-radius: inherit;
z-index: -1; /* sit behind menu content but within menu stacking */
}
/* Solid hover state for items to further avoid transparency feel */
.dropdown-item:hover, .dropdown-item:focus {
background-color: var(--light-color) !important;
}
/* Backdrop to block interactions behind open dropdowns */
/* Removed custom dropdown backdrop; rely on Bootstrap defaults */
/* Increase dropdown item touch targets and spacing */
.dropdown-item {
padding: 0.6rem 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.dropdown-item i { width: 1rem; text-align: center; }
/* Enhanced Mobile Components */
.mobile-stack {
@@ -927,3 +982,52 @@ h6 { font-size: 1rem; }
}
/* Shared summary cards used across pages (invoices, reports) */
.summary-card {
transition: all 0.3s ease;
border-radius: 12px;
}
.summary-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.summary-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.summary-label {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.summary-value {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
}
.empty-state {
padding: 2rem;
}
.empty-state i {
opacity: 0.5;
}
@media (max-width: 768px) {
.summary-card { margin-bottom: 1rem; }
.summary-value { font-size: 18px; }
}
+2
View File
@@ -235,6 +235,8 @@
nav.classList.remove('scrolled');
}
}, { passive: true });
// Use Bootstrap's default dropdown behavior; no custom backdrop
</script>
{% if current_user.is_authenticated %}
+71 -62
View File
@@ -4,84 +4,93 @@
{% block content %}
<div class="container mt-4">
<!-- 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>
<!-- Header Section (Invoices-style) -->
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex align-items-center">
<h1 class="h3 mb-0 me-3">
<i class="fas fa-tasks text-primary"></i>
Tasks
</h1>
<span class="badge bg-primary fs-6">{{ tasks|length }} total</span>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('tasks.create_task') }}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i> New Task
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Stats -->
<!-- Summary Cards (Invoices-style) -->
<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 class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-primary bg-opacity-10 text-primary">
<i class="fas fa-list-check"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">To Do</div>
<div class="summary-value">{{ tasks|selectattr('status', 'equalto', 'todo')|list|length }}</div>
</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 class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-warning bg-opacity-10 text-warning">
<i class="fas fa-spinner"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">In Progress</div>
<div class="summary-value">{{ tasks|selectattr('status', 'equalto', 'in_progress')|list|length }}</div>
</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 class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-info bg-opacity-10 text-info">
<i class="fas fa-user-check"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Review</div>
<div class="summary-value">{{ tasks|selectattr('status', 'equalto', 'review')|list|length }}</div>
</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 class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-success bg-opacity-10 text-success">
<i class="fas fa-check-circle"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Completed</div>
<div class="summary-value">{{ tasks|selectattr('status', 'equalto', 'done')|list|length }}</div>
</div>
</div>
</div>
+80 -49
View File
@@ -4,72 +4,103 @@
{% block content %}
<div class="container mt-4">
<!-- 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>
<!-- Header Section (Invoices-style) -->
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex align-items-center">
<h1 class="h3 mb-0 me-3">
<i class="fas fa-user text-primary"></i>
My Tasks
</h1>
<span class="badge bg-primary fs-6">{{ tasks|length }} total</span>
</div>
<div class="d-flex gap-2">
<div class="dropdown">
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-filter me-1"></i> Filter
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('tasks.list_tasks') }}">All Tasks</a></li>
<li><a class="dropdown-item" href="{{ url_for('tasks.my_tasks', task_type='assigned') }}">Assigned to Me</a></li>
<li><a class="dropdown-item" href="{{ url_for('tasks.my_tasks', task_type='created') }}">Created by Me</a></li>
</ul>
</div>
<a href="{{ url_for('tasks.create_task') }}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i> New Task
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Stats -->
<!-- Summary Cards (Invoices-style) -->
<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 class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-primary bg-opacity-10 text-primary">
<i class="fas fa-list-check"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">To Do</div>
<div class="summary-value">{{ tasks|selectattr('status', 'equalto', 'todo')|list|length }}</div>
</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 class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-warning bg-opacity-10 text-warning">
<i class="fas fa-spinner"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">In Progress</div>
<div class="summary-value">{{ tasks|selectattr('status', 'equalto', 'in_progress')|list|length }}</div>
</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 class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-info bg-opacity-10 text-info">
<i class="fas fa-user-check"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Review</div>
<div class="summary-value">{{ tasks|selectattr('status', 'equalto', 'review')|list|length }}</div>
</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 class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-success bg-opacity-10 text-success">
<i class="fas fa-check-circle"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Completed</div>
<div class="summary-value">{{ tasks|selectattr('status', 'equalto', 'done')|list|length }}</div>
</div>
</div>
</div>
+88 -58
View File
@@ -7,50 +7,19 @@
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">
<i class="fas fa-building text-primary"></i> Clients
</h1>
{% if current_user.is_admin %}
<a href="{{ url_for('clients.create_client') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> New Client
</a>
{% endif %}
</div>
</div>
</div>
<!-- Filters -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-filter me-2 text-primary"></i>Filters
</h6>
<div class="d-flex align-items-center">
<h1 class="h3 mb-0 me-3">
<i class="fas fa-building text-primary"></i>
Clients
</h1>
<span class="badge bg-primary fs-6">{{ clients|length }} total</span>
</div>
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-4">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="">All Statuses</option>
<option value="active" {% if request.args.get('status') == 'active' %}selected{% endif %}>Active</option>
<option value="inactive" {% if request.args.get('status') == 'inactive' %}selected{% endif %}>Inactive</option>
</select>
</div>
<div class="col-md-6">
<label for="search" class="form-label">Search</label>
<input type="text" class="form-control" id="search" name="search"
value="{{ request.args.get('search', '') }}"
placeholder="Search by name, description, contact person, or email">
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-search"></i> Search
</button>
</div>
</form>
<div class="d-flex gap-2">
{% if current_user.is_admin %}
<a href="{{ url_for('clients.create_client') }}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i> New Client
</a>
{% endif %}
</div>
</div>
</div>
@@ -59,17 +28,28 @@
<!-- Client List -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-list me-1"></i> Client List ({{ clients|length }})
</h5>
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<div class="d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-list me-2"></i>All Clients
</h6>
<div class="d-flex gap-2">
<div class="input-group input-group-sm" style="width: 250px;">
<span class="input-group-text bg-light border-end-0">
<i class="fas fa-search text-muted"></i>
</span>
<input type="text" class="form-control border-start-0" id="searchInput"
placeholder="Search clients...">
</div>
</div>
</div>
</div>
<div class="card-body">
<div class="card-body p-0">
{% if clients %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<table class="table table-hover mb-0" id="clientsTable">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Contact Person</th>
@@ -83,7 +63,7 @@
</thead>
<tbody>
{% for client in clients %}
<tr>
<tr class="client-row" data-status="{{ client.status }}">
<td>
<a href="{{ url_for('clients.view_client', client_id=client.id) }}" class="text-decoration-none">
<strong>{{ client.name }}</strong>
@@ -109,17 +89,18 @@
{% endif %}
</td>
<td>
<span class="badge bg-primary-subtle text-primary border border-primary-subtle rounded-pill">{{ client.total_projects }}</span>
<span class="badge badge-soft-primary badge-pill">{{ client.total_projects }}</span>
{% if client.active_projects > 0 %}
<br><small class="text-muted">{{ client.active_projects }} active</small>
{% endif %}
</td>
<td>
{% if client.status == 'active' %}
<span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle">Active</span>
{% else %}
<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle">Inactive</span>
{% endif %}
{% set status_map = {
'active': {'bg': 'bg-success', 'label': 'Active'},
'inactive': {'bg': 'bg-secondary', 'label': 'Inactive'}
} %}
{% set sc = status_map.get(client.status, status_map['inactive']) %}
<span class="status-badge {{ sc.bg }} text-white">{{ sc.label }}</span>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
@@ -180,4 +161,53 @@
</div>
</div>
</div>
<style>
/* Align clients list visuals with invoices list */
.status-badge { padding: 6px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; letter-spacing: 0.5px; }
.badge-pill { border-radius: 9999px; }
.badge-soft-primary { background: rgba(59,130,246,0.12); color: var(--primary-color); border: 1px solid rgba(59,130,246,0.25); }
.empty-state { padding: 2rem; }
.btn-group .btn { border-radius: 6px !important; }
.btn-group .btn:not(:last-child) { margin-right: 2px; }
@media (max-width: 768px) {
.table-responsive { font-size: 14px; }
.btn-group .btn { padding: 0.375rem 0.5rem; font-size: 12px; }
.status-badge { font-size: 10px; padding: 4px 8px; }
}
</style>
{% block extra_js %}
<script>
// Simple search (vanilla JS)
(function() {
const searchInput = document.getElementById('searchInput');
const table = document.getElementById('clientsTable');
if (!table) return;
const rows = Array.from(table.querySelectorAll('tbody tr'));
function normalize(text) { return (text || '').toLowerCase(); }
function matchesSearch(row, term) {
if (!term) return true;
const cellsText = Array.from(row.querySelectorAll('td')).map(td => td.innerText).join(' ');
return normalize(cellsText).includes(term);
}
function applyFilters() {
const term = normalize(searchInput ? searchInput.value : '');
rows.forEach(row => {
const show = matchesSearch(row, term);
row.style.display = show ? '' : 'none';
});
}
if (searchInput) {
searchInput.addEventListener('input', applyFilters);
}
// Initial
applyFilters();
})();
</script>
{% endblock %}
{% endblock %}
+173 -75
View File
@@ -21,80 +21,157 @@
</a>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex align-items-center">
<h1 class="h3 mb-0 me-3">
<i class="fas fa-building text-primary"></i>
{{ client.name }}
</h1>
{% set status_map = {
'active': {'bg': 'bg-success', 'label': 'Active'},
'inactive': {'bg': 'bg-secondary', 'label': 'Inactive'}
} %}
{% set sc = status_map.get(client.status, status_map['inactive']) %}
<span class="status-badge-large {{ sc.bg }} text-white">{{ sc.label }}</span>
</div>
<div class="btn-group" role="group">
{% if current_user.is_admin %}
<a href="{{ url_for('clients.edit_client', client_id=client.id) }}" class="btn btn-secondary">
<i class="fas fa-edit me-1"></i> Edit
</a>
{% endif %}
<a href="{{ url_for('clients.list_clients') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> Back
</a>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3 d-flex align-items-center">
<div class="summary-icon bg-primary bg-opacity-10 text-primary me-3">
<i class="fas fa-project-diagram"></i>
</div>
<div>
<div class="summary-label">Total Projects</div>
<div class="summary-value">{{ client.total_projects }}</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3 d-flex align-items-center">
<div class="summary-icon bg-success bg-opacity-10 text-success me-3">
<i class="fas fa-toggle-on"></i>
</div>
<div>
<div class="summary-label">Active Projects</div>
<div class="summary-value">{{ client.active_projects }}</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3 d-flex align-items-center">
<div class="summary-icon bg-info bg-opacity-10 text-info me-3">
<i class="fas fa-clock"></i>
</div>
<div>
<div class="summary-label">Total Hours</div>
<div class="summary-value">{{ "%.1f"|format(client.total_hours) }}</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3 d-flex align-items-center">
<div class="summary-icon bg-warning bg-opacity-10 text-warning me-3">
<i class="fas fa-sack-dollar"></i>
</div>
<div>
<div class="summary-label">Est. Total Cost</div>
<div class="summary-value">{{ "%.2f"|format(client.estimated_total_cost) }} {{ currency }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Client Information -->
<div class="col-lg-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-info-circle"></i> Client Information
</h5>
<div class="card mb-4 shadow-sm border-0">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-info-circle me-2"></i>Client Information
</h6>
</div>
<div class="card-body">
<div class="mb-3">
<strong>Status:</strong>
{% if client.status == 'active' %}
<span class="badge bg-success ms-2">Active</span>
{% else %}
<span class="badge bg-secondary ms-2">Inactive</span>
{% endif %}
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value"><span class="status-badge-large {{ sc.bg }} text-white">{{ sc.label }}</span></span>
</div>
</div>
{% if client.description %}
<div class="mb-3">
<strong>Description:</strong>
<p class="mb-0 mt-1">{{ client.description }}</p>
<div class="section-title text-primary mb-2">Description</div>
<div class="content-box">{{ client.description }}</div>
</div>
{% endif %}
{% if client.contact_person %}
<div class="mb-3">
<strong>Contact Person:</strong>
<p class="mb-0 mt-1">{{ client.contact_person }}</p>
<div class="detail-row">
<span class="detail-label">Contact Person</span>
<span class="detail-value">{{ client.contact_person }}</span>
</div>
</div>
{% endif %}
{% if client.email %}
<div class="mb-3">
<strong>Email:</strong>
<p class="mb-0 mt-1">
<a href="mailto:{{ client.email }}">{{ client.email }}</a>
</p>
<div class="detail-row">
<span class="detail-label">Email</span>
<span class="detail-value"><a href="mailto:{{ client.email }}">{{ client.email }}</a></span>
</div>
</div>
{% endif %}
{% if client.phone %}
<div class="mb-3">
<strong>Phone:</strong>
<p class="mb-0 mt-1">{{ client.phone }}</p>
<div class="detail-row">
<span class="detail-label">Phone</span>
<span class="detail-value">{{ client.phone }}</span>
</div>
</div>
{% endif %}
{% if client.address %}
<div class="mb-3">
<strong>Address:</strong>
<p class="mb-0 mt-1">{{ client.address }}</p>
<div class="section-title text-primary mb-2">Address</div>
<div class="content-box">{{ client.address }}</div>
</div>
{% endif %}
{% if client.default_hourly_rate %}
<div class="mb-3">
<strong>Default Hourly Rate:</strong>
<p class="mb-0 mt-1">{{ "%.2f"|format(client.default_hourly_rate) }} {{ currency }}</p>
<div class="detail-row">
<span class="detail-label">Default Hourly Rate</span>
<span class="detail-value">{{ "%.2f"|format(client.default_hourly_rate) }} {{ currency }}</span>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Client Statistics -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-chart-bar"></i> Statistics
</h5>
<div class="card mb-4 shadow-sm border-0">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-bar me-2"></i>Statistics
</h6>
</div>
<div class="card-body">
<div class="row text-center">
@@ -123,33 +200,31 @@
<!-- Client Actions -->
{% if current_user.is_admin %}
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-cogs"></i> Actions
</h5>
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-cog me-2"></i>Status & Actions
</h6>
</div>
<div class="card-body">
{% if client.status == 'active' %}
<form method="POST" action="{{ url_for('clients.archive_client', client_id=client.id) }}"
class="mb-2" onsubmit="return confirm('Are you sure you want to archive this client?')">
<button type="submit" class="btn btn-warning w-100">
<i class="fas fa-archive"></i> Archive Client
<form method="POST" action="{{ url_for('clients.archive_client', client_id=client.id) }}" class="mb-2" onsubmit="return confirm('Are you sure you want to archive this client?')">
<button type="submit" class="btn btn-outline-secondary w-100">
<i class="fas fa-archive me-2"></i>Archive Client
</button>
</form>
{% else %}
<form method="POST" action="{{ url_for('clients.activate_client', client_id=client.id) }}" class="mb-2">
<button type="submit" class="btn btn-success w-100">
<i class="fas fa-check"></i> Activate Client
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-check me-2"></i>Activate Client
</button>
</form>
{% endif %}
{% if client.total_projects == 0 %}
<form method="POST" action="{{ url_for('clients.delete_client', client_id=client.id) }}"
onsubmit="return confirm('Are you sure you want to delete this client? This action cannot be undone.')">
<form method="POST" action="{{ url_for('clients.delete_client', client_id=client.id) }}" onsubmit="return confirm('Are you sure you want to delete this client? This action cannot be undone.')">
<button type="submit" class="btn btn-danger w-100">
<i class="fas fa-trash"></i> Delete Client
<i class="fas fa-trash me-2"></i>Delete Client
</button>
</form>
{% endif %}
@@ -160,22 +235,23 @@
<!-- Projects List -->
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-project-diagram"></i> Projects ({{ projects|length }})
</h5>
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-project-diagram me-2"></i>Projects
<span class="badge badge-soft-secondary ms-2">{{ projects|length }} total</span>
</h6>
{% if current_user.is_admin %}
<a href="{{ url_for('projects.create_project') }}" class="btn btn-primary btn-sm">
<i class="fas fa-plus"></i> New Project
<a href="{{ url_for('projects.create_project') }}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-plus me-1"></i> New Project
</a>
{% endif %}
</div>
<div class="card-body">
<div class="card-body p-0">
{% if projects %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Project Name</th>
<th>Status</th>
@@ -194,21 +270,21 @@
<strong>{{ project.name }}</strong>
</a>
{% if project.description %}
<br><small class="text-muted">{{ project.description[:50] }}{% if project.description|length > 50 %}...{% endif %}</small>
<br><small class="text-muted">{{ project.description[:50] }}{% if project.description|length > 50 %}...{% endif %}</small>
{% endif %}
</td>
<td>
{% if project.status == 'active' %}
<span class="badge bg-success">Active</span>
<span class="badge badge-soft-success">Active</span>
{% else %}
<span class="badge bg-secondary">Archived</span>
<span class="badge badge-soft-secondary">Archived</span>
{% endif %}
</td>
<td>
{% if project.billable %}
<span class="badge bg-primary">Yes</span>
<span class="badge badge-soft-primary">Yes</span>
{% else %}
<span class="badge bg-secondary">No</span>
<span class="badge badge-soft-secondary">No</span>
{% endif %}
</td>
<td>
@@ -229,12 +305,12 @@
<td>
<div class="btn-group" role="group">
<a href="{{ url_for('projects.view_project', project_id=project.id) }}"
class="btn btn-sm btn-outline-primary" title="View">
class="btn btn-sm btn-action btn-action--view" title="View">
<i class="fas fa-eye"></i>
</a>
{% if current_user.is_admin %}
<a href="{{ url_for('projects.edit_project', project_id=project.id) }}"
class="btn btn-sm btn-outline-secondary" title="Edit">
class="btn btn-sm btn-action btn-action--edit" title="Edit">
<i class="fas fa-edit"></i>
</a>
{% endif %}
@@ -247,14 +323,16 @@
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-project-diagram fa-3x text-muted mb-3"></i>
<h4 class="text-muted">No Projects Found</h4>
<p class="text-muted">This client doesn't have any projects yet.</p>
{% if current_user.is_admin %}
<a href="{{ url_for('projects.create_project') }}" class="btn btn-primary">
<i class="fas fa-plus"></i> Create First Project
</a>
{% endif %}
<div class="empty-state">
<i class="fas fa-project-diagram fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No projects found</h5>
<p class="text-muted mb-3">This client doesn't have any projects yet.</p>
{% if current_user.is_admin %}
<a href="{{ url_for('projects.create_project') }}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i> Create First Project
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
@@ -262,4 +340,24 @@
</div>
</div>
</div>
<style>
.status-badge-large { padding: 8px 16px; border-radius: 25px; font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.section-title { font-size: 16px; font-weight: 600; border-bottom: 2px solid var(--primary-color); padding-bottom: 8px; }
.detail-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding: 8px 0; border-bottom: 1px solid var(--border-color); }
.detail-label { font-weight: 600; color: var(--text-secondary); }
.detail-value { font-weight: 600; color: var(--text-primary); }
.content-box { background: var(--light-color); padding: 16px; border-radius: 8px; border-left: 4px solid var(--primary-color); line-height: 1.6; }
.summary-card { transition: all 0.3s ease; border-radius: 12px; }
.summary-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important; }
.summary-icon { width: 44px; height: 44px; border-radius: 10px; display: inline-flex; align-items: center; justify-content: center; }
.summary-label { font-size: 12px; color: var(--text-secondary); font-weight: 600; letter-spacing: 0.4px; text-transform: uppercase; }
.summary-value { font-size: 20px; font-weight: 800; color: var(--text-primary); }
.empty-state { padding: 2rem; }
@media (max-width: 768px) {
.btn-group { flex-direction: column; width: 100%; }
.btn-group .btn { margin-bottom: 8px; border-radius: 6px !important; }
.detail-row { flex-direction: column; align-items: flex-start; gap: 4px; }
.status-badge-large { font-size: 12px; padding: 6px 12px; }
}
</style>
{% endblock %}
+208 -99
View File
@@ -6,17 +6,77 @@
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4 flex-column flex-md-row">
<h1 class="h3 mb-0 mb-3 mb-md-0">
<i class="fas fa-project-diagram text-primary"></i> Projects
</h1>
{% if current_user.is_admin %}
<div>
<a href="{{ url_for('projects.create_project') }}" class="btn btn-primary touch-target">
<i class="fas fa-plus"></i> New Project
</a>
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex align-items-center">
<h1 class="h3 mb-0 me-3">
<i class="fas fa-project-diagram text-primary"></i>
Projects
</h1>
<span class="badge bg-primary fs-6">{{ projects|length }} total</span>
</div>
<div class="d-flex gap-2">
{% if current_user.is_admin %}
<a href="{{ url_for('projects.create_project') }}" class="btn btn-primary touch-target">
<i class="fas fa-plus me-1"></i> New Project
</a>
{% endif %}
</div>
</div>
</div>
</div>
{# Summary cards similar to invoices #}
{% set _active = 0 %}
{% set _archived = 0 %}
{% set _total_hours = 0 %}
{% for p in projects %}
{% if p.status == 'active' %}{% set _active = _active + 1 %}{% endif %}
{% if p.status == 'archived' %}{% set _archived = _archived + 1 %}{% endif %}
{% set _total_hours = _total_hours + (p.total_hours or 0) %}
{% endfor %}
<div class="row mb-4">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3 d-flex align-items-center">
<div class="summary-icon bg-primary bg-opacity-10 text-primary"><i class="fas fa-list"></i></div>
<div class="ms-3">
<div class="summary-label">Total Projects</div>
<div class="summary-value">{{ projects|length }}</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3 d-flex align-items-center">
<div class="summary-icon bg-success bg-opacity-10 text-success"><i class="fas fa-check-circle"></i></div>
<div class="ms-3">
<div class="summary-label">Active</div>
<div class="summary-value">{{ _active }}</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3 d-flex align-items-center">
<div class="summary-icon bg-secondary bg-opacity-10 text-secondary"><i class="fas fa-archive"></i></div>
<div class="ms-3">
<div class="summary-label">Archived</div>
<div class="summary-value">{{ _archived }}</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3 d-flex align-items-center">
<div class="summary-icon bg-info bg-opacity-10 text-info"><i class="fas fa-hourglass-half"></i></div>
<div class="ms-3">
<div class="summary-label">Total Hours</div>
<div class="summary-value">{{ '%.1f'|format(_total_hours) }}h</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
@@ -71,24 +131,35 @@
<!-- Projects List -->
<div class="row">
<div class="col-12">
<div class="card mobile-card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-list"></i> Projects ({{ projects|length }})
</h5>
<div class="card shadow-sm border-0 mobile-card">
<div class="card-header bg-white py-3">
<div class="d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-list me-2"></i>All Projects
</h6>
<div class="d-flex gap-2">
<div class="input-group input-group-sm" style="width: 250px;">
<span class="input-group-text bg-light border-end-0">
<i class="fas fa-search text-muted"></i>
</span>
<input type="text" class="form-control border-start-0" id="searchInput"
placeholder="Search projects...">
</div>
</div>
</div>
</div>
<div class="card-body">
<div class="card-body p-0">
{% if projects %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<table class="table table-hover mb-0" id="projectsTable">
<thead class="table-light">
<tr>
<th>Project</th>
<th>Client</th>
<th>Status</th>
<th>Hours</th>
<th>Rate</th>
<th>Actions</th>
<th class="border-0">Project</th>
<th class="border-0">Client</th>
<th class="border-0">Status</th>
<th class="border-0">Hours</th>
<th class="border-0">Rate</th>
<th class="border-0 text-center">Actions</th>
</tr>
</thead>
<tbody>
@@ -105,13 +176,13 @@
</div>
</td>
<td data-label="Client">
<span class="badge rounded-pill bg-info-subtle text-info-emphasis border border-info-subtle">{{ project.client }}</span>
<span class="project-badge">{{ project.client }}</span>
</td>
<td data-label="Status">
{% if project.status == 'active' %}
<span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle">Active</span>
<span class="status-badge bg-success text-white">Active</span>
{% else %}
<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle">Archived</span>
<span class="status-badge bg-secondary text-white">Archived</span>
{% endif %}
</td>
<td data-label="Hours" class="align-middle" style="min-width:180px;">
@@ -128,7 +199,7 @@
</td>
<td data-label="Rate" class="text-end">
{% if project.hourly_rate %}
<span class="badge bg-primary-subtle text-primary border border-primary-subtle rounded-pill">${{ "%.2f"|format(project.hourly_rate) }}/h</span>
<span class="badge bg-primary-subtle text-primary border border-primary-subtle rounded-pill">{{ currency }}{{ "%.2f"|format(project.hourly_rate) }}/h</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
@@ -158,16 +229,16 @@
</div>
{% else %}
<div class="text-center py-5">
<div class="mb-4">
<i class="fas fa-project-diagram fa-3x text-muted opacity-50"></i>
<div class="empty-state">
<i class="fas fa-project-diagram fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No projects found</h5>
<p class="text-muted mb-4">Create your first project to get started.</p>
{% if current_user.is_admin %}
<a href="{{ url_for('projects.create_project') }}" class="btn btn-primary touch-target">
<i class="fas fa-plus me-2"></i> Create Project
</a>
{% endif %}
</div>
<h5 class="text-muted mb-3">No projects found</h5>
<p class="text-muted mb-4">Create your first project to get started</p>
{% if current_user.is_admin %}
<a href="{{ url_for('projects.create_project') }}" class="btn btn-primary touch-target">
<i class="fas fa-plus me-2"></i>Create Project
</a>
{% endif %}
</div>
{% endif %}
</div>
@@ -208,74 +279,112 @@
</div>
</div>
<style>
.summary-card {
transition: all 0.3s ease;
border-radius: 12px;
}
.summary-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
}
.summary-icon {
width: 44px;
height: 44px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.summary-label {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.summary-value {
font-size: 20px;
font-weight: 800;
color: var(--text-primary);
}
.project-badge {
background: var(--light-color);
color: var(--text-secondary);
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
}
.status-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.empty-state {
padding: 2rem;
}
@media (max-width: 768px) {
.summary-card { margin-bottom: 1rem; }
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Mobile-specific improvements
if (window.innerWidth <= 768) {
// Improve mobile table responsiveness
const tableRows = document.querySelectorAll('tbody tr');
tableRows.forEach(row => {
const cells = row.querySelectorAll('td');
cells.forEach((cell, index) => {
if (index === 0) {
cell.setAttribute('data-label', 'Project');
} else if (index === 1) {
cell.setAttribute('data-label', 'Client');
} else if (index === 2) {
cell.setAttribute('data-label', 'Status');
} else if (index === 3) {
cell.setAttribute('data-label', 'Hours');
} else if (index === 4) {
cell.setAttribute('data-label', 'Rate');
} else if (index === 5) {
cell.setAttribute('data-label', 'Actions');
} else if (index === 6) {
cell.setAttribute('data-label', 'Actions');
}
});
});
// Improve touch targets
const inputs = document.querySelectorAll('.form-control, .form-select');
inputs.forEach(input => {
input.classList.add('touch-target');
});
const buttons = document.querySelectorAll('.btn');
buttons.forEach(btn => {
btn.classList.add('touch-target');
// Initialize DataTable
const table = $('#projectsTable').DataTable({
order: [[0, 'asc']],
pageLength: 25,
responsive: true,
dom: '<"row"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>' +
'<"row"<"col-sm-12"tr>>' +
'<"row"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>',
columnDefs: [
{ orderable: false, targets: -1 }
]
});
// Custom search
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('keyup', function() {
table.search(this.value).draw();
});
}
// Handle mobile viewport changes
window.addEventListener('resize', function() {
if (window.innerWidth <= 768) {
// Re-apply mobile table improvements
const tableRows = document.querySelectorAll('tbody tr');
tableRows.forEach(row => {
const cells = row.querySelectorAll('td');
cells.forEach((cell, index) => {
if (index === 0) {
cell.setAttribute('data-label', 'Project');
} else if (index === 1) {
cell.setAttribute('data-label', 'Client');
} else if (index === 2) {
cell.setAttribute('data-label', 'Status');
} else if (index === 3) {
cell.setAttribute('data-label', 'Hours');
} else if (index === 4) {
cell.setAttribute('data-label', 'Rate');
} else if (index === 5) {
cell.setAttribute('data-label', 'Actions');
} else if (index === 6) {
cell.setAttribute('data-label', 'Actions');
}
});
});
}
// Fill progress bars
document.querySelectorAll('#projectsTable .progress-bar').forEach(el => {
const pct = el.getAttribute('data-pct') || 0;
el.style.width = pct + '%';
});
});
function filterByStatus(status) {
const table = $('#projectsTable').DataTable();
if (status === 'all') {
table.column(2).search('').draw();
} else {
// Match the rendered badge text
const regex = status === 'active' ? '^Active$' : '^Archived$';
table.column(2).search(regex, true, false).draw();
}
document.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('active'));
const active = document.querySelector(`[onclick="filterByStatus('${status}')"]`);
if (active) active.classList.add('active');
}
// Function to show delete project modal
function showDeleteProjectModal(projectId, projectName) {
document.getElementById('deleteProjectName').textContent = projectName;
@@ -287,7 +396,7 @@ function showDeleteProjectModal(projectId, projectName) {
document.addEventListener('DOMContentLoaded', function() {
const deleteForm = document.getElementById('deleteProjectForm');
if (deleteForm) {
deleteForm.addEventListener('submit', function(e) {
deleteForm.addEventListener('submit', function() {
const submitBtn = this.querySelector('button[type="submit"]');
submitBtn.innerHTML = '<div class="loading-spinner me-2"></div>Deleting...';
submitBtn.disabled = true;
+97 -88
View File
@@ -7,25 +7,38 @@
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('projects.list_projects') }}">Projects</a></li>
<li class="breadcrumb-item active">{{ project.name }}</li>
</ol>
</nav>
<h1 class="h3 mb-0">
<i class="fas fa-project-diagram text-primary"></i> {{ project.name }}
</h1>
<div class="d-flex align-items-center">
<div>
<nav aria-label="breadcrumb" class="mb-1">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('projects.list_projects') }}">Projects</a></li>
<li class="breadcrumb-item active">{{ project.name }}</li>
</ol>
</nav>
<h1 class="h3 mb-0 me-3">
<i class="fas fa-project-diagram text-primary"></i>
{{ project.name }}
</h1>
</div>
<div class="ms-3">
{% if project.status == 'active' %}
<span class="status-badge bg-success text-white"><i class="fas fa-check-circle me-2"></i>Active</span>
{% else %}
<span class="status-badge bg-secondary text-white"><i class="fas fa-archive me-2"></i>Archived</span>
{% endif %}
</div>
</div>
<div>
<div class="btn-group" role="group">
{% if current_user.is_admin %}
<a href="{{ url_for('projects.edit_project', project_id=project.id) }}" class="btn btn-secondary">
<i class="fas fa-edit"></i> Edit
<i class="fas fa-edit me-1"></i> Edit
</a>
{% endif %}
<a href="{{ url_for('main.dashboard') }}" class="btn btn-primary">
<i class="fas fa-clock"></i> Start Timer
<a href="{{ url_for('main.dashboard') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> Back
</a>
<a href="{{ url_for('timer.start_timer', project_id=project.id) }}" class="btn btn-primary">
<i class="fas fa-play me-1"></i> Start Timer
</a>
</div>
</div>
@@ -35,74 +48,51 @@
<!-- Project Details -->
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-info-circle"></i> Project Details
</h5>
</div>
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">Name:</dt>
<dd class="col-sm-8">{{ project.name }}</dd>
<dt class="col-sm-4">Client:</dt>
<dd class="col-sm-8">
{% if project.client_obj %}
<a href="{{ url_for('clients.view_client', client_id=project.client_id) }}">
{{ project.client_obj.name }}
</a>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</dd>
<dt class="col-sm-4">Status:</dt>
<dd class="col-sm-8">
{% if project.status == 'active' %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-secondary">Archived</span>
{% endif %}
</dd>
<dt class="col-sm-4">Created:</dt>
<dd class="col-sm-8">{{ project.created_at.strftime('%Y-%m-%d %H:%M') }}</dd>
</dl>
<div class="invoice-section">
<h6 class="section-title text-primary mb-3">
<i class="fas fa-info-circle me-2"></i>General
</h6>
<div class="detail-row"><span class="detail-label">Name</span><span class="detail-value">{{ project.name }}</span></div>
<div class="detail-row"><span class="detail-label">Client</span>
<span class="detail-value">
{% if project.client_obj %}
<a href="{{ url_for('clients.view_client', client_id=project.client_id) }}">{{ project.client_obj.name }}</a>
{% else %}<span class="text-muted">-</span>{% endif %}
</span>
</div>
<div class="detail-row"><span class="detail-label">Status</span>
<span class="detail-value">{% if project.status == 'active' %}Active{% else %}Archived{% endif %}</span>
</div>
<div class="detail-row"><span class="detail-label">Created</span><span class="detail-value">{{ project.created_at.strftime('%B %d, %Y') }}</span></div>
</div>
</div>
<div class="col-md-6">
<dl class="row">
<dt class="col-sm-4">Billable:</dt>
<dd class="col-sm-8">
{% if project.billable %}
<span class="badge bg-success">Yes</span>
{% else %}
<span class="badge bg-secondary">No</span>
{% endif %}
</dd>
<div class="invoice-section">
<h6 class="section-title text-primary mb-3">
<i class="fas fa-cog me-2"></i>Billing
</h6>
<div class="detail-row"><span class="detail-label">Billable</span>
<span class="detail-value">{% if project.billable %}Yes{% else %}No{% endif %}</span>
</div>
{% if project.billable and project.hourly_rate %}
<dt class="col-sm-4">Hourly Rate:</dt>
<dd class="col-sm-8">{{ currency }} {{ "%.2f"|format(project.hourly_rate) }}</dd>
<div class="detail-row"><span class="detail-label">Hourly Rate</span><span class="detail-value">{{ currency }} {{ "%.2f"|format(project.hourly_rate) }}</span></div>
{% endif %}
{% if project.billing_ref %}
<dt class="col-sm-4">Billing Ref:</dt>
<dd class="col-sm-8">{{ project.billing_ref }}</dd>
<div class="detail-row"><span class="detail-label">Billing Ref</span><span class="detail-value">{{ project.billing_ref }}</span></div>
{% endif %}
<dt class="col-sm-4">Last Updated:</dt>
<dd class="col-sm-8">{{ project.updated_at.strftime('%Y-%m-%d %H:%M') }}</dd>
</dl>
<div class="detail-row"><span class="detail-label">Last Updated</span><span class="detail-value">{{ project.updated_at.strftime('%B %d, %Y') }}</span></div>
</div>
</div>
</div>
{% if project.description %}
<div class="mt-3">
<h6>Description:</h6>
<p class="text-muted">{{ project.description }}</p>
<h6 class="section-title text-primary mb-2">Description</h6>
<div class="content-box">{{ project.description }}</div>
</div>
{% endif %}
</div>
@@ -110,11 +100,11 @@
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-chart-bar"></i> Statistics
</h5>
<div class="card shadow-sm border-0">
<div class="card-header bg-light py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-bar me-2"></i>Statistics
</h6>
</div>
<div class="card-body">
<div class="row text-center">
@@ -137,11 +127,11 @@
</div>
{% if project.billable and project.hourly_rate %}
<div class="card mt-3">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-users"></i> User Breakdown
</h5>
<div class="card mt-3 shadow-sm border-0">
<div class="card-header bg-light py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-users me-2"></i>User Breakdown
</h6>
</div>
<div class="card-body">
{% for user_total in project.get_user_totals() %}
@@ -278,16 +268,14 @@
<!-- Time Entries -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-clock"></i> Time Entries
</h5>
<div>
<a href="{{ url_for('reports.project_report', project_id=project.id) }}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-chart-line"></i> View Report
</a>
</div>
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-clock me-2"></i>Time Entries
</h6>
<a href="{{ url_for('reports.project_report', project_id=project.id) }}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-chart-line"></i> View Report
</a>
</div>
<div class="card-body">
{% if entries %}
@@ -514,4 +502,25 @@ document.addEventListener('DOMContentLoaded', function() {
flex: 1;
}
</style>
<style>
.status-badge {
padding: 8px 16px;
border-radius: 25px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.invoice-section { padding: 12px 0; }
.section-title { font-size: 16px; font-weight: 600; border-bottom: 2px solid var(--primary-color); padding-bottom: 8px; }
.detail-row { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; padding:8px 0; border-bottom:1px solid var(--border-color); }
.detail-label { font-weight:600; color:var(--text-secondary); }
.detail-value { font-weight:600; color:var(--text-primary); }
.content-box { background: var(--light-color); padding: 16px; border-radius: 8px; border-left: 4px solid var(--primary-color); line-height: 1.6; }
@media (max-width:768px){
.detail-row{flex-direction:column; align-items:flex-start; gap:4px;}
}
</style>
{% endblock %}
+77 -43
View File
@@ -21,39 +21,71 @@
<!-- Summary Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-clock fa-2x text-primary mb-2"></i>
<h4 class="text-primary">{{ "%.1f"|format(summary.total_hours) }}h</h4>
<p class="text-muted mb-0">Total Hours</p>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-primary bg-opacity-10 text-primary">
<i class="fas fa-clock"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Total Hours</div>
<div class="summary-value">{{ "%.1f"|format(summary.total_hours) }}h</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-dollar-sign fa-2x text-success mb-2"></i>
<h4 class="text-success">{{ "%.1f"|format(summary.billable_hours) }}h</h4>
<p class="text-muted mb-0">Billable Hours</p>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-success bg-opacity-10 text-success">
<i class="fas fa-dollar-sign"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Billable Hours</div>
<div class="summary-value">{{ "%.1f"|format(summary.billable_hours) }}h</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-project-diagram fa-2x text-info mb-2"></i>
<h4 class="text-info">{{ summary.active_projects }}</h4>
<p class="text-muted mb-0">Active Projects</p>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-info bg-opacity-10 text-info">
<i class="fas fa-project-diagram"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Active Projects</div>
<div class="summary-value">{{ summary.active_projects }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-users fa-2x text-warning mb-2"></i>
<h4 class="text-warning">{{ summary.total_users }}</h4>
<p class="text-muted mb-0">Users</p>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-warning bg-opacity-10 text-warning">
<i class="fas fa-users"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Users</div>
<div class="summary-value">{{ summary.total_users }}</div>
</div>
</div>
</div>
</div>
</div>
@@ -62,8 +94,8 @@
<!-- Report Options -->
<div class="row">
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<div class="card h-100 border-0 shadow-sm hover-lift">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="fas fa-project-diagram"></i> Project Reports
</h5>
@@ -80,8 +112,8 @@
</div>
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<div class="card h-100 border-0 shadow-sm hover-lift">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="fas fa-user"></i> User Reports
</h5>
@@ -98,8 +130,8 @@
</div>
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<div class="card h-100 border-0 shadow-sm hover-lift">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="fas fa-calendar-alt"></i> Summary Report
</h5>
@@ -116,8 +148,8 @@
</div>
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<div class="card h-100 border-0 shadow-sm hover-lift">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="fas fa-chart-line"></i> Visual Analytics
</h5>
@@ -134,8 +166,8 @@
</div>
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<div class="card h-100 border-0 shadow-sm hover-lift">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="fas fa-download"></i> Data Export
</h5>
@@ -155,17 +187,17 @@
<!-- Recent Activity -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="fas fa-history"></i> Recent Activity
</h5>
</div>
<div class="card-body">
<div class="card-body p-0">
{% if recent_entries %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>User</th>
<th>Project</th>
@@ -208,10 +240,12 @@
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="fas fa-clock fa-2x text-muted mb-3"></i>
<h5 class="text-muted">No Recent Activity</h5>
<p class="text-muted">No time entries have been recorded recently.</p>
<div class="text-center py-5">
<div class="empty-state">
<i class="fas fa-clock fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Recent Activity</h5>
<p class="text-muted">No time entries have been recorded recently.</p>
</div>
</div>
{% endif %}
</div>
+74 -40
View File
@@ -30,8 +30,8 @@
<!-- Filters -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="fas fa-filter"></i> Filters
</h5>
@@ -86,39 +86,71 @@
<!-- Summary Statistics -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-clock fa-2x text-primary mb-2"></i>
<h4 class="text-primary">{{ "%.1f"|format(summary.total_hours) }}h</h4>
<p class="text-muted mb-0">Total Hours</p>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-primary bg-opacity-10 text-primary">
<i class="fas fa-clock"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Total Hours</div>
<div class="summary-value">{{ "%.1f"|format(summary.total_hours) }}h</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-dollar-sign fa-2x text-success mb-2"></i>
<h4 class="text-success">{{ "%.1f"|format(summary.billable_hours) }}h</h4>
<p class="text-muted mb-0">Billable Hours</p>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-success bg-opacity-10 text-success">
<i class="fas fa-dollar-sign"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Billable Hours</div>
<div class="summary-value">{{ "%.1f"|format(summary.billable_hours) }}h</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-dollar-sign fa-2x text-success mb-2"></i>
<h4 class="text-success">{{ currency }} {{ "%.2f"|format(summary.total_billable_amount) }}</h4>
<p class="text-muted mb-0">Billable Amount</p>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-success bg-opacity-10 text-success">
<i class="fas fa-sack-dollar"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Billable Amount</div>
<div class="summary-value">{{ currency }} {{ "%.2f"|format(summary.total_billable_amount) }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-project-diagram fa-2x text-info mb-2"></i>
<h4 class="text-info">{{ summary.projects_count }}</h4>
<p class="text-muted mb-0">Projects</p>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-info bg-opacity-10 text-info">
<i class="fas fa-project-diagram"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Projects</div>
<div class="summary-value">{{ summary.projects_count }}</div>
</div>
</div>
</div>
</div>
</div>
@@ -127,17 +159,17 @@
<!-- Project Breakdown -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="fas fa-list"></i> Project Breakdown ({{ projects_data|length }})
</h5>
</div>
<div class="card-body">
<div class="card-body p-0">
{% if projects_data %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Project</th>
<th>Client</th>
@@ -211,16 +243,18 @@
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-chart-bar fa-3x text-muted mb-3"></i>
<h4 class="text-muted">No Data Found</h4>
<p class="text-muted">
<div class="empty-state">
<i class="fas fa-chart-bar fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Data Found</h5>
<p class="text-muted">
{% if request.args.get('start_date') or request.args.get('end_date') or request.args.get('project_id') or request.args.get('user_id') %}
Try adjusting your filters or
<a href="{{ url_for('reports.project_report') }}">view all projects</a>.
{% else %}
No time entries have been recorded yet.
{% endif %}
</p>
</p>
</div>
</div>
{% endif %}
</div>
@@ -232,16 +266,16 @@
{% if entries %}
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="fas fa-clock"></i> Time Entries ({{ entries|length }})
</h5>
</div>
<div class="card-body">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>User</th>
<th>Project</th>
+52 -26
View File
@@ -24,30 +24,54 @@
<!-- Key Metrics -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-sun fa-2x text-primary mb-2"></i>
<h4 class="text-primary">{{ "%.1f"|format(today_hours) }}h</h4>
<p class="text-muted mb-0">Today</p>
<div class="col-md-4 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-primary bg-opacity-10 text-primary">
<i class="fas fa-sun"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Today</div>
<div class="summary-value">{{ "%.1f"|format(today_hours) }}h</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-calendar-week fa-2x text-success mb-2"></i>
<h4 class="text-success">{{ "%.1f"|format(week_hours) }}h</h4>
<p class="text-muted mb-0">Last 7 Days</p>
<div class="col-md-4 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-success bg-opacity-10 text-success">
<i class="fas fa-calendar-week"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Last 7 Days</div>
<div class="summary-value">{{ "%.1f"|format(week_hours) }}h</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-calendar-alt fa-2x text-info mb-2"></i>
<h4 class="text-info">{{ "%.1f"|format(month_hours) }}h</h4>
<p class="text-muted mb-0">Last 30 Days</p>
<div class="col-md-4 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-info bg-opacity-10 text-info">
<i class="fas fa-calendar-alt"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Last 30 Days</div>
<div class="summary-value">{{ "%.1f"|format(month_hours) }}h</div>
</div>
</div>
</div>
</div>
</div>
@@ -56,17 +80,17 @@
<!-- Top Projects -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="fas fa-project-diagram"></i> Top Projects ({{ project_stats|length }})
</h5>
</div>
<div class="card-body">
<div class="card-body p-0">
{% if project_stats %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Project</th>
<th>Client</th>
@@ -90,9 +114,11 @@
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-chart-bar fa-3x text-muted mb-3"></i>
<h4 class="text-muted">No Data Found</h4>
<p class="text-muted">No time entries available for the selected period.</p>
<div class="empty-state">
<i class="fas fa-chart-bar fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Data Found</h5>
<p class="text-muted">No time entries available for the selected period.</p>
</div>
</div>
{% endif %}
</div>
+73 -39
View File
@@ -30,8 +30,8 @@
<!-- Filters -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0">
<i class="fas fa-filter"></i> Filters
</h5>
@@ -86,39 +86,71 @@
<!-- Summary Statistics -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-clock fa-2x text-primary mb-2"></i>
<h4 class="text-primary">{{ "%.1f"|format(summary.total_hours) }}h</h4>
<p class="text-muted mb-0">Total Hours</p>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-primary bg-opacity-10 text-primary">
<i class="fas fa-clock"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Total Hours</div>
<div class="summary-value">{{ "%.1f"|format(summary.total_hours) }}h</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-dollar-sign fa-2x text-success mb-2"></i>
<h4 class="text-success">{{ "%.1f"|format(summary.billable_hours) }}h</h4>
<p class="text-muted mb-0">Billable Hours</p>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-success bg-opacity-10 text-success">
<i class="fas fa-dollar-sign"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Billable Hours</div>
<div class="summary-value">{{ "%.1f"|format(summary.billable_hours) }}h</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-users fa-2x text-info mb-2"></i>
<h4 class="text-info">{{ summary.users_count }}</h4>
<p class="text-muted mb-0">Users</p>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-info bg-opacity-10 text-info">
<i class="fas fa-users"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Users</div>
<div class="summary-value">{{ summary.users_count }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<i class="fas fa-project-diagram fa-2x text-warning mb-2"></i>
<h4 class="text-warning">{{ summary.projects_count }}</h4>
<p class="text-muted mb-0">Projects</p>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card border-0 shadow-sm h-100 summary-card">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="summary-icon bg-warning bg-opacity-10 text-warning">
<i class="fas fa-project-diagram"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<div class="summary-label">Projects</div>
<div class="summary-value">{{ summary.projects_count }}</div>
</div>
</div>
</div>
</div>
</div>
@@ -127,17 +159,17 @@
<!-- User Breakdown -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="fas fa-list"></i> User Breakdown ({{ user_totals|length }})
</h5>
</div>
<div class="card-body">
<div class="card-body p-0">
{% if user_totals %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>User</th>
<th>Total Hours</th>
@@ -163,9 +195,11 @@
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-users fa-3x text-muted mb-3"></i>
<h4 class="text-muted">No Data Found</h4>
<p class="text-muted">Try adjusting your filters.</p>
<div class="empty-state">
<i class="fas fa-users fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Data Found</h5>
<p class="text-muted">Try adjusting your filters.</p>
</div>
</div>
{% endif %}
</div>
@@ -177,16 +211,16 @@
{% if entries %}
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0">
<i class="fas fa-clock"></i> Time Entries ({{ entries|length }})
</h5>
</div>
<div class="card-body">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>User</th>
<th>Project</th>
+141 -65
View File
@@ -3,103 +3,130 @@
{% block title %}Log Time - {{ app_name }}{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8 col-md-10">
<div class="card">
<div class="card-header d-flex align-items-center">
<i class="fas fa-plus me-2 text-primary"></i>
<h5 class="mb-0">Log Time Manually</h5>
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex align-items-center">
<h1 class="h3 mb-0 me-3">
<i class="fas fa-clock text-primary"></i>
Log Time
</h1>
</div>
<a href="{{ url_for('main.dashboard') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> Back
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card shadow-sm border-0">
<div class="card-header py-3 bg-primary text-white">
<h6 class="m-0 font-weight-bold">
<i class="fas fa-plus me-2"></i>Manual Entry
</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('timer.manual_entry') }}">
<div class="mb-4">
<label for="project_id" class="form-label fw-semibold">
<i class="fas fa-project-diagram me-1"></i>Project *
</label>
<select class="form-select" id="project_id" name="project_id" required>
<option value="">Select a project...</option>
{% set selected_project_id = (request.form.get('project_id') or '')|int %}
{% for project in projects %}
<option value="{{ project.id }}" {% if project.id == selected_project_id %}selected{% endif %}>{{ project.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-4">
<label for="task_id" class="form-label fw-semibold">
<i class="fas fa-tasks me-1"></i>Task (optional)
</label>
{% set preselected_task_id = request.form.get('task_id') or request.args.get('task_id') %}
<select class="form-select" id="task_id" name="task_id" data-selected-task-id="{{ preselected_task_id or '' }}" disabled>
<option value="">No task</option>
</select>
<div class="form-text">Tasks will be loaded for the selected project.</div>
<div class="row">
<div class="col-md-6">
<div class="form-floating mb-3">
<select class="form-select" id="project_id" name="project_id" required>
<option value=""></option>
{% set selected_project_id = (request.form.get('project_id') or '')|int %}
{% for project in projects %}
<option value="{{ project.id }}" {% if project.id == selected_project_id %}selected{% endif %}>{{ project.name }}</option>
{% endfor %}
</select>
<label for="project_id"><i class="fas fa-project-diagram me-1"></i>Project *</label>
<div class="form-text">Select the project to log time for</div>
</div>
</div>
<div class="col-md-6">
{% set preselected_task_id = request.form.get('task_id') or request.args.get('task_id') %}
<div class="form-floating mb-3">
<select class="form-select" id="task_id" name="task_id" data-selected-task-id="{{ preselected_task_id or '' }}" disabled>
<option value=""></option>
</select>
<label for="task_id"><i class="fas fa-tasks me-1"></i>Task (optional)</label>
<div class="form-text">Tasks load after selecting a project</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-12 col-md-6">
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="fas fa-play me-1"></i>Start *
</label>
<div class="row g-2">
<div class="col-6">
<input type="date" class="form-control" name="start_date" id="start_date" required value="{{ request.form.get('start_date','') }}">
</div>
<div class="col-6">
<input type="time" class="form-control" name="start_time" id="start_time" required value="{{ request.form.get('start_time','') }}">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="mb-2 fw-semibold text-primary"><i class="fas fa-play me-1"></i>Start *</div>
<div class="row g-2">
<div class="col-6">
<div class="form-floating">
<input type="date" class="form-control" name="start_date" id="start_date" required value="{{ request.form.get('start_date','') }}">
<label for="start_date">Date</label>
</div>
</div>
<div class="col-6">
<div class="form-floating">
<input type="time" class="form-control" name="start_time" id="start_time" required value="{{ request.form.get('start_time','') }}">
<label for="start_time">Time</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="fas fa-stop me-1"></i>End *
</label>
<div class="row g-2">
<div class="col-6">
<input type="date" class="form-control" name="end_date" id="end_date" required value="{{ request.form.get('end_date','') }}">
</div>
<div class="col-6">
<input type="time" class="form-control" name="end_time" id="end_time" required value="{{ request.form.get('end_time','') }}">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="mb-2 fw-semibold text-primary"><i class="fas fa-stop me-1"></i>End *</div>
<div class="row g-2">
<div class="col-6">
<div class="form-floating">
<input type="date" class="form-control" name="end_date" id="end_date" required value="{{ request.form.get('end_date','') }}">
<label for="end_date">Date</label>
</div>
</div>
<div class="col-6">
<div class="form-floating">
<input type="time" class="form-control" name="end_time" id="end_time" required value="{{ request.form.get('end_time','') }}">
<label for="end_time">Time</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mb-4">
<label for="notes" class="form-label fw-semibold">
<i class="fas fa-sticky-note me-1"></i>Notes
</label>
<textarea class="form-control" id="notes" name="notes" rows="3" placeholder="What did you work on?">{{ request.form.get('notes','') }}</textarea>
<div class="form-floating my-3">
<textarea class="form-control" id="notes" name="notes" style="height: 100px" placeholder="What did you work on?">{{ request.form.get('notes','') }}</textarea>
<label for="notes"><i class="fas fa-sticky-note me-1"></i>Notes</label>
</div>
<div class="row g-3">
<div class="row g-3 align-items-center">
<div class="col-12 col-md-8">
<div class="mb-4">
<label for="tags" class="form-label fw-semibold">
<i class="fas fa-tags me-1"></i>Tags
</label>
<div class="form-floating mb-3 mb-md-0">
<input type="text" class="form-control" id="tags" name="tags" placeholder="tag1, tag2, tag3" value="{{ request.form.get('tags','') }}">
<label for="tags"><i class="fas fa-tags me-1"></i>Tags</label>
<div class="form-text">Separate tags with commas</div>
</div>
</div>
<div class="col-12 col-md-4 d-flex align-items-center">
<div class="form-check form-switch mt-4 w-100">
<div class="col-12 col-md-4">
<div class="form-check form-switch mt-2 d-flex align-items-center">
<input class="form-check-input" type="checkbox" id="billable" name="billable" {% if request.form.get('billable') %}checked{% else %}checked{% endif %}>
<label class="form-check-label fw-semibold" for="billable">
<label class="form-check-label fw-semibold ms-2" for="billable">
<i class="fas fa-dollar-sign me-1"></i>Billable
</label>
</div>
</div>
</div>
<div class="d-flex justify-content-between flex-column flex-md-row">
<a href="{{ url_for('main.dashboard') }}" class="btn btn-secondary mb-2 mb-md-0">
<i class="fas fa-arrow-left me-1"></i>Back
<div class="d-flex justify-content-between flex-column flex-md-row mt-3">
<a href="{{ url_for('main.dashboard') }}" class="btn btn-outline-secondary mb-2 mb-md-0">
<i class="fas fa-arrow-left me-1"></i> Back
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Save Entry
@@ -109,9 +136,58 @@
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm border-0">
<div class="card-header py-3 bg-light">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-lightbulb me-2"></i>Quick Tips
</h6>
</div>
<div class="card-body">
<div class="tip-item mb-3 d-flex gap-3">
<div class="tip-icon text-primary"><i class="fas fa-tasks"></i></div>
<div class="tip-content">
<strong>Use Tasks</strong>
<p class="small text-muted mb-0">Categorize time by selecting a task after choosing a project.</p>
</div>
</div>
<div class="tip-item mb-3 d-flex gap-3">
<div class="tip-icon text-success"><i class="fas fa-dollar-sign"></i></div>
<div class="tip-content">
<strong>Billable Time</strong>
<p class="small text-muted mb-0">Enable billable to include this entry in invoices.</p>
</div>
</div>
<div class="tip-item d-flex gap-3">
<div class="tip-icon text-info"><i class="fas fa-tags"></i></div>
<div class="tip-content">
<strong>Tag Entries</strong>
<p class="small text-muted mb-0">Add tags to filter entries in reports later.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.form-floating > .form-control:focus ~ label,
.form-floating > .form-control:not(:placeholder-shown) ~ label,
.form-floating > .form-select:focus ~ label,
.form-floating > .form-select:not([value=""]) ~ label {
color: var(--primary-color);
}
.form-control:focus, .form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.15);
}
.tip-icon { font-size: 18px; }
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Set default dates to today