mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-04-30 17:29:50 -05:00
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:
+58
-12
@@ -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
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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"> </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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user