Enable Project categorization.
This commit is contained in:
@@ -1,10 +1,46 @@
|
|||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="timetrack-container">
|
<div class="timetrack-container admin-projects-container">
|
||||||
<div class="admin-header">
|
<div class="admin-header">
|
||||||
<h2>Project Management</h2>
|
<h2>Project Management</h2>
|
||||||
|
<div class="admin-actions">
|
||||||
<a href="{{ url_for('create_project') }}" class="btn btn-success">Create New Project</a>
|
<a href="{{ url_for('create_project') }}" class="btn btn-success">Create New Project</a>
|
||||||
|
<button id="manage-categories-btn" class="btn btn-info">Manage Categories</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Project Categories Section -->
|
||||||
|
<div id="categories-section" class="categories-section" style="display: none;">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>Project Categories</h3>
|
||||||
|
<button id="create-category-btn" class="btn btn-success btn-sm">Create Category</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="categories-grid">
|
||||||
|
{% for category in categories %}
|
||||||
|
<div class="category-card" data-category-id="{{ category.id }}">
|
||||||
|
<div class="category-header" style="background-color: {{ category.color }}20; border-left: 4px solid {{ category.color }};">
|
||||||
|
<div class="category-title">
|
||||||
|
<span class="category-icon">{{ category.icon or '📁' }}</span>
|
||||||
|
<span class="category-name">{{ category.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="category-actions">
|
||||||
|
<button class="edit-category-btn btn btn-xs btn-primary" data-id="{{ category.id }}">Edit</button>
|
||||||
|
<button class="delete-category-btn btn btn-xs btn-danger" data-id="{{ category.id }}">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="category-body">
|
||||||
|
<p class="category-description">{{ category.description or 'No description' }}</p>
|
||||||
|
<small class="category-projects">{{ category.projects|length }} project(s)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="no-categories">
|
||||||
|
<p>No categories created yet. <button id="first-category-btn" class="btn btn-link">Create your first category</button>.</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if projects %}
|
{% if projects %}
|
||||||
@@ -14,6 +50,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Code</th>
|
<th>Code</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>Category</th>
|
||||||
<th>Team</th>
|
<th>Team</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Start Date</th>
|
<th>Start Date</th>
|
||||||
@@ -28,6 +65,15 @@
|
|||||||
<tr class="{% if not project.is_active %}inactive-project{% endif %}">
|
<tr class="{% if not project.is_active %}inactive-project{% endif %}">
|
||||||
<td><strong>{{ project.code }}</strong></td>
|
<td><strong>{{ project.code }}</strong></td>
|
||||||
<td>{{ project.name }}</td>
|
<td>{{ project.name }}</td>
|
||||||
|
<td>
|
||||||
|
{% if project.category %}
|
||||||
|
<span class="category-badge" style="background-color: {{ project.category.color }}20; color: {{ project.category.color }};">
|
||||||
|
{{ project.category.icon or '📁' }} {{ project.category.name }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<em>No category</em>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if project.team %}
|
{% if project.team %}
|
||||||
{{ project.team.name }}
|
{{ project.team.name }}
|
||||||
@@ -46,7 +92,8 @@
|
|||||||
<td>{{ project.time_entries|length }}</td>
|
<td>{{ project.time_entries|length }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a href="{{ url_for('edit_project', project_id=project.id) }}" class="btn btn-sm btn-primary">Edit</a>
|
<a href="{{ url_for('edit_project', project_id=project.id) }}" class="btn btn-sm btn-primary">Edit</a>
|
||||||
{% if g.user.role.name == 'ADMIN' and project.time_entries|length == 0 %}
|
<a href="{{ url_for('manage_project_tasks', project_id=project.id) }}" class="btn btn-sm btn-info">Tasks</a>
|
||||||
|
{% if g.user.role == Role.ADMIN and project.time_entries|length == 0 %}
|
||||||
<form method="POST" action="{{ url_for('delete_project', project_id=project.id) }}" style="display: inline;"
|
<form method="POST" action="{{ url_for('delete_project', project_id=project.id) }}" style="display: inline;"
|
||||||
onsubmit="return confirm('Are you sure you want to delete this project?')">
|
onsubmit="return confirm('Are you sure you want to delete this project?')">
|
||||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||||
@@ -68,14 +115,82 @@
|
|||||||
<style>
|
<style>
|
||||||
/* Override container width for admin projects page */
|
/* Override container width for admin projects page */
|
||||||
.admin-projects-container {
|
.admin-projects-container {
|
||||||
max-width: 1200px !important;
|
max-width: none !important;
|
||||||
width: auto !important;
|
width: 100% !important;
|
||||||
padding: 1.5rem !important;
|
padding: 1rem !important;
|
||||||
margin: 0 auto 2rem auto !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects-table {
|
.projects-table {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-table table {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 1200px; /* Ensure table is wide enough for all columns */
|
||||||
|
table-layout: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize column widths */
|
||||||
|
.projects-table th:nth-child(1), /* Code */
|
||||||
|
.projects-table td:nth-child(1) {
|
||||||
|
width: 8%;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-table th:nth-child(2), /* Name */
|
||||||
|
.projects-table td:nth-child(2) {
|
||||||
|
width: 20%;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-table th:nth-child(3), /* Category */
|
||||||
|
.projects-table td:nth-child(3) {
|
||||||
|
width: 12%;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-table th:nth-child(4), /* Team */
|
||||||
|
.projects-table td:nth-child(4) {
|
||||||
|
width: 12%;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-table th:nth-child(5), /* Status */
|
||||||
|
.projects-table td:nth-child(5) {
|
||||||
|
width: 8%;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-table th:nth-child(6), /* Start Date */
|
||||||
|
.projects-table td:nth-child(6) {
|
||||||
|
width: 10%;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-table th:nth-child(7), /* End Date */
|
||||||
|
.projects-table td:nth-child(7) {
|
||||||
|
width: 10%;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-table th:nth-child(8), /* Created By */
|
||||||
|
.projects-table td:nth-child(8) {
|
||||||
|
width: 10%;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-table th:nth-child(9), /* Time Entries */
|
||||||
|
.projects-table td:nth-child(9) {
|
||||||
|
width: 8%;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-table th:nth-child(10), /* Actions */
|
||||||
|
.projects-table td:nth-child(10) {
|
||||||
|
width: 12%;
|
||||||
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inactive-project {
|
.inactive-project {
|
||||||
@@ -101,6 +216,18 @@
|
|||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
min-width: 180px; /* Ensure enough space for buttons */
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions .btn {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions .btn-sm {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
min-width: 60px; /* Consistent button widths */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button definitions now in main style.css */
|
/* Button definitions now in main style.css */
|
||||||
@@ -110,5 +237,387 @@
|
|||||||
padding: 3rem;
|
padding: 3rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Category Management Styles */
|
||||||
|
.admin-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-body {
|
||||||
|
padding: 0 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-description {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-projects {
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-badge {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-categories {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.btn-xs {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Consistent button styling across the interface */
|
||||||
|
.btn-info {
|
||||||
|
background-color: #17a2b8;
|
||||||
|
border-color: #17a2b8;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info:hover {
|
||||||
|
background-color: #138496;
|
||||||
|
border-color: #117a8b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background-color: #28a745;
|
||||||
|
border-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
border-color: #1e7e34;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header .btn {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-actions .btn {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-width: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the "Manage Categories" button more prominent */
|
||||||
|
#manage-categories-btn {
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-input-group input[type="color"] {
|
||||||
|
width: 50px;
|
||||||
|
height: 38px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-input-group input[type="text"] {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive improvements */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-table table {
|
||||||
|
min-width: 800px; /* Smaller minimum on mobile */
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-grid {
|
||||||
|
grid-template-columns: 1fr; /* Single column on mobile */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better spacing for action buttons */
|
||||||
|
.actions form {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions .btn + .btn,
|
||||||
|
.actions .btn + form {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- Category Management Modal -->
|
||||||
|
<div id="category-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close">×</span>
|
||||||
|
<h3 id="category-modal-title">Create Category</h3>
|
||||||
|
<form id="category-form">
|
||||||
|
<input type="hidden" id="category-id" name="category_id">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category-name">Name:</label>
|
||||||
|
<input type="text" id="category-name" name="name" required maxlength="100">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category-description">Description:</label>
|
||||||
|
<textarea id="category-description" name="description" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category-color">Color:</label>
|
||||||
|
<div class="color-input-group">
|
||||||
|
<input type="color" id="category-color" name="color" value="#007bff">
|
||||||
|
<input type="text" id="category-color-text" name="color_text" value="#007bff" maxlength="7">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category-icon">Icon (emoji or text):</label>
|
||||||
|
<input type="text" id="category-icon" name="icon" maxlength="10" placeholder="📁">
|
||||||
|
<small>Use emojis or short text like 📁, 🎯, 💼, etc.</small>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Save Category</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="cancel-category">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const manageCategoriesBtn = document.getElementById('manage-categories-btn');
|
||||||
|
const categoriesSection = document.getElementById('categories-section');
|
||||||
|
const categoryModal = document.getElementById('category-modal');
|
||||||
|
const categoryForm = document.getElementById('category-form');
|
||||||
|
const createCategoryBtn = document.getElementById('create-category-btn');
|
||||||
|
const firstCategoryBtn = document.getElementById('first-category-btn');
|
||||||
|
const colorInput = document.getElementById('category-color');
|
||||||
|
const colorTextInput = document.getElementById('category-color-text');
|
||||||
|
|
||||||
|
// Toggle categories section
|
||||||
|
manageCategoriesBtn.addEventListener('click', function() {
|
||||||
|
const isVisible = categoriesSection.style.display !== 'none';
|
||||||
|
categoriesSection.style.display = isVisible ? 'none' : 'block';
|
||||||
|
this.textContent = isVisible ? 'Manage Categories' : 'Hide Categories';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sync color inputs
|
||||||
|
colorInput.addEventListener('change', function() {
|
||||||
|
colorTextInput.value = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
colorTextInput.addEventListener('change', function() {
|
||||||
|
if (/^#[0-9A-F]{6}$/i.test(this.value)) {
|
||||||
|
colorInput.value = this.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open create category modal
|
||||||
|
function openCategoryModal(categoryData = null) {
|
||||||
|
const isEdit = categoryData !== null;
|
||||||
|
document.getElementById('category-modal-title').textContent = isEdit ? 'Edit Category' : 'Create Category';
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
document.getElementById('category-id').value = categoryData.id;
|
||||||
|
document.getElementById('category-name').value = categoryData.name;
|
||||||
|
document.getElementById('category-description').value = categoryData.description || '';
|
||||||
|
document.getElementById('category-color').value = categoryData.color;
|
||||||
|
document.getElementById('category-color-text').value = categoryData.color;
|
||||||
|
document.getElementById('category-icon').value = categoryData.icon || '';
|
||||||
|
} else {
|
||||||
|
categoryForm.reset();
|
||||||
|
document.getElementById('category-id').value = '';
|
||||||
|
document.getElementById('category-color').value = '#007bff';
|
||||||
|
document.getElementById('category-color-text').value = '#007bff';
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryModal.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
createCategoryBtn?.addEventListener('click', () => openCategoryModal());
|
||||||
|
firstCategoryBtn?.addEventListener('click', () => openCategoryModal());
|
||||||
|
|
||||||
|
// Edit category buttons
|
||||||
|
document.querySelectorAll('.edit-category-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const categoryId = this.getAttribute('data-id');
|
||||||
|
// Get category data from the card
|
||||||
|
const card = this.closest('.category-card');
|
||||||
|
const categoryData = {
|
||||||
|
id: categoryId,
|
||||||
|
name: card.querySelector('.category-name').textContent,
|
||||||
|
description: card.querySelector('.category-description').textContent,
|
||||||
|
color: card.querySelector('.category-header').style.borderLeftColor || '#007bff',
|
||||||
|
icon: card.querySelector('.category-icon').textContent
|
||||||
|
};
|
||||||
|
openCategoryModal(categoryData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete category buttons
|
||||||
|
document.querySelectorAll('.delete-category-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const categoryId = this.getAttribute('data-id');
|
||||||
|
if (confirm('Are you sure you want to delete this category? Projects using this category will be unassigned.')) {
|
||||||
|
fetch(`/api/admin/categories/${categoryId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('An error occurred while deleting the category');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
document.querySelector('#category-modal .close').addEventListener('click', function() {
|
||||||
|
categoryModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('cancel-category').addEventListener('click', function() {
|
||||||
|
categoryModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit category form
|
||||||
|
categoryForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const categoryId = formData.get('category_id');
|
||||||
|
const isEdit = categoryId !== '';
|
||||||
|
|
||||||
|
const url = isEdit ? `/api/admin/categories/${categoryId}` : '/api/admin/categories';
|
||||||
|
const method = isEdit ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: formData.get('name'),
|
||||||
|
description: formData.get('description'),
|
||||||
|
color: formData.get('color'),
|
||||||
|
icon: formData.get('icon')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
categoryModal.style.display = 'none';
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('An error occurred while saving the category');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
window.addEventListener('click', function(event) {
|
||||||
|
if (event.target === categoryModal) {
|
||||||
|
categoryModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="mode-switcher">
|
<div class="mode-switcher">
|
||||||
<button class="mode-btn {% if mode == 'personal' %}active{% endif %}"
|
<button class="mode-btn {% if mode == 'personal' %}active{% endif %}"
|
||||||
onclick="switchMode('personal')">Personal</button>
|
onclick="switchMode('personal')">Personal</button>
|
||||||
{% if g.user.team_id and g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator'] %}
|
{% if g.user.team_id and g.user.role in [Role.TEAM_LEADER, Role.SUPERVISOR, Role.ADMIN] %}
|
||||||
<button class="mode-btn {% if mode == 'team' %}active{% endif %}"
|
<button class="mode-btn {% if mode == 'team' %}active{% endif %}"
|
||||||
onclick="switchMode('team')">Team</button>
|
onclick="switchMode('team')">Team</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -40,6 +40,19 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category_id">Project Category</label>
|
||||||
|
<select id="category_id" name="category_id">
|
||||||
|
<option value="">No Category</option>
|
||||||
|
{% for category in categories %}
|
||||||
|
<option value="{{ category.id }}"
|
||||||
|
{% if request.form.category_id and request.form.category_id|int == category.id %}selected{% endif %}>
|
||||||
|
{{ category.icon or '📁' }} {{ category.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
|||||||
@@ -41,6 +41,21 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category_id">Project Category</label>
|
||||||
|
<select id="category_id" name="category_id">
|
||||||
|
<option value="">No Category</option>
|
||||||
|
{% for category in categories %}
|
||||||
|
<option value="{{ category.id }}"
|
||||||
|
{% if (request.form.category_id and request.form.category_id|int == category.id) or (not request.form.category_id and project.category_id == category.id) %}selected{% endif %}>
|
||||||
|
{{ category.icon or '📁' }} {{ category.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" name="is_active"
|
<input type="checkbox" name="is_active"
|
||||||
|
|||||||
783
templates/manage_project_tasks.html
Normal file
783
templates/manage_project_tasks.html
Normal file
@@ -0,0 +1,783 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="timetrack-container">
|
||||||
|
<div class="task-header">
|
||||||
|
<div class="project-info">
|
||||||
|
<h2>Tasks for Project: {{ project.code }} - {{ project.name }}</h2>
|
||||||
|
<p class="project-description">{{ project.description or 'No description available' }}</p>
|
||||||
|
{% if project.category %}
|
||||||
|
<span class="category-badge" style="background-color: {{ project.category.color }}20; color: {{ project.category.color }};">
|
||||||
|
{{ project.category.icon or '📁' }} {{ project.category.name }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="task-actions">
|
||||||
|
<button id="create-task-btn" class="btn btn-success">Create New Task</button>
|
||||||
|
<a href="{{ url_for('admin_projects') }}" class="btn btn-secondary">Back to Projects</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Task Statistics -->
|
||||||
|
<div class="task-stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>{{ tasks|length }}</h3>
|
||||||
|
<p>Total Tasks</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>{{ tasks|selectattr('status.value', 'equalto', 'Completed')|list|length }}</h3>
|
||||||
|
<p>Completed</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>{{ tasks|selectattr('status.value', 'equalto', 'In Progress')|list|length }}</h3>
|
||||||
|
<p>In Progress</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>{{ tasks|selectattr('status.value', 'equalto', 'Not Started')|list|length }}</h3>
|
||||||
|
<p>Not Started</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Task List -->
|
||||||
|
{% if tasks %}
|
||||||
|
<div class="tasks-section">
|
||||||
|
<h3>Project Tasks</h3>
|
||||||
|
<div class="tasks-container">
|
||||||
|
{% for task in tasks %}
|
||||||
|
<div class="task-card" data-task-id="{{ task.id }}">
|
||||||
|
<div class="task-header">
|
||||||
|
<div class="task-title-area">
|
||||||
|
<h4 class="task-name">{{ task.name }}</h4>
|
||||||
|
<div class="task-meta">
|
||||||
|
<span class="status-badge status-{{ task.status.value.lower().replace(' ', '-') }}">
|
||||||
|
{{ task.status.value }}
|
||||||
|
</span>
|
||||||
|
<span class="priority-badge priority-{{ task.priority.value.lower() }}">
|
||||||
|
{{ task.priority.value }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="task-actions">
|
||||||
|
<button class="btn btn-xs btn-primary edit-task-btn" data-id="{{ task.id }}">Edit</button>
|
||||||
|
<button class="btn btn-xs btn-info add-subtask-btn" data-id="{{ task.id }}">Add Subtask</button>
|
||||||
|
<button class="btn btn-xs btn-danger delete-task-btn" data-id="{{ task.id }}">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="task-body">
|
||||||
|
<p class="task-description">{{ task.description or 'No description' }}</p>
|
||||||
|
|
||||||
|
<div class="task-details">
|
||||||
|
<div class="task-detail">
|
||||||
|
<strong>Assigned to:</strong>
|
||||||
|
{% if task.assigned_to %}
|
||||||
|
{{ task.assigned_to.username }}
|
||||||
|
{% else %}
|
||||||
|
<em>Unassigned</em>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="task-detail">
|
||||||
|
<strong>Due Date:</strong>
|
||||||
|
{% if task.due_date %}
|
||||||
|
{{ task.due_date.strftime('%Y-%m-%d') }}
|
||||||
|
{% else %}
|
||||||
|
<em>No due date</em>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="task-detail">
|
||||||
|
<strong>Estimated Hours:</strong>
|
||||||
|
{% if task.estimated_hours %}
|
||||||
|
{{ task.estimated_hours }}h
|
||||||
|
{% else %}
|
||||||
|
<em>Not estimated</em>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="task-detail">
|
||||||
|
<strong>Time Logged:</strong>
|
||||||
|
{{ (task.total_time_logged / 3600)|round(1) }}h
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<div class="progress-section">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span>Progress: {{ task.progress_percentage }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" style="width: {{ task.progress_percentage }}%;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Subtasks -->
|
||||||
|
{% if task.subtasks %}
|
||||||
|
<div class="subtasks-section">
|
||||||
|
<h5>Subtasks ({{ task.subtasks|length }})</h5>
|
||||||
|
<div class="subtasks-list">
|
||||||
|
{% for subtask in task.subtasks %}
|
||||||
|
<div class="subtask-item" data-subtask-id="{{ subtask.id }}">
|
||||||
|
<div class="subtask-content">
|
||||||
|
<span class="subtask-name">{{ subtask.name }}</span>
|
||||||
|
<span class="status-badge status-{{ subtask.status.value.lower().replace(' ', '-') }}">
|
||||||
|
{{ subtask.status.value }}
|
||||||
|
</span>
|
||||||
|
{% if subtask.assigned_to %}
|
||||||
|
<span class="subtask-assignee">{{ subtask.assigned_to.username }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="subtask-actions">
|
||||||
|
<button class="btn btn-xs btn-primary edit-subtask-btn" data-id="{{ subtask.id }}">Edit</button>
|
||||||
|
<button class="btn btn-xs btn-danger delete-subtask-btn" data-id="{{ subtask.id }}">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="no-tasks">
|
||||||
|
<div class="no-tasks-content">
|
||||||
|
<h3>No Tasks Created Yet</h3>
|
||||||
|
<p>Start organizing your project by creating tasks.</p>
|
||||||
|
<button id="create-first-task-btn" class="btn btn-primary">Create Your First Task</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Task Creation/Edit Modal -->
|
||||||
|
<div id="task-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close">×</span>
|
||||||
|
<h3 id="task-modal-title">Create Task</h3>
|
||||||
|
<form id="task-form">
|
||||||
|
<input type="hidden" id="task-id" name="task_id">
|
||||||
|
<input type="hidden" id="project-id" name="project_id" value="{{ project.id }}">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="task-name">Task Name *</label>
|
||||||
|
<input type="text" id="task-name" name="name" required maxlength="200">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="task-description">Description</label>
|
||||||
|
<textarea id="task-description" name="description" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="task-status">Status</label>
|
||||||
|
<select id="task-status" name="status">
|
||||||
|
<option value="Not Started">Not Started</option>
|
||||||
|
<option value="In Progress">In Progress</option>
|
||||||
|
<option value="On Hold">On Hold</option>
|
||||||
|
<option value="Completed">Completed</option>
|
||||||
|
<option value="Cancelled">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="task-priority">Priority</label>
|
||||||
|
<select id="task-priority" name="priority">
|
||||||
|
<option value="Low">Low</option>
|
||||||
|
<option value="Medium" selected>Medium</option>
|
||||||
|
<option value="High">High</option>
|
||||||
|
<option value="Urgent">Urgent</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="task-assigned-to">Assigned To</label>
|
||||||
|
<select id="task-assigned-to" name="assigned_to_id">
|
||||||
|
<option value="">Unassigned</option>
|
||||||
|
{% for user in team_members %}
|
||||||
|
<option value="{{ user.id }}">{{ user.username }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="task-estimated-hours">Estimated Hours</label>
|
||||||
|
<input type="number" id="task-estimated-hours" name="estimated_hours" step="0.5" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="task-start-date">Start Date</label>
|
||||||
|
<input type="date" id="task-start-date" name="start_date">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="task-due-date">Due Date</label>
|
||||||
|
<input type="date" id="task-due-date" name="due_date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Save Task</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="cancel-task">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Subtask Creation/Edit Modal -->
|
||||||
|
<div id="subtask-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close">×</span>
|
||||||
|
<h3 id="subtask-modal-title">Create Subtask</h3>
|
||||||
|
<form id="subtask-form">
|
||||||
|
<input type="hidden" id="subtask-id" name="subtask_id">
|
||||||
|
<input type="hidden" id="parent-task-id" name="task_id">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subtask-name">Subtask Name *</label>
|
||||||
|
<input type="text" id="subtask-name" name="name" required maxlength="200">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subtask-description">Description</label>
|
||||||
|
<textarea id="subtask-description" name="description" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subtask-status">Status</label>
|
||||||
|
<select id="subtask-status" name="status">
|
||||||
|
<option value="Not Started">Not Started</option>
|
||||||
|
<option value="In Progress">In Progress</option>
|
||||||
|
<option value="On Hold">On Hold</option>
|
||||||
|
<option value="Completed">Completed</option>
|
||||||
|
<option value="Cancelled">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subtask-priority">Priority</label>
|
||||||
|
<select id="subtask-priority" name="priority">
|
||||||
|
<option value="Low">Low</option>
|
||||||
|
<option value="Medium" selected>Medium</option>
|
||||||
|
<option value="High">High</option>
|
||||||
|
<option value="Urgent">Urgent</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subtask-assigned-to">Assigned To</label>
|
||||||
|
<select id="subtask-assigned-to" name="assigned_to_id">
|
||||||
|
<option value="">Unassigned</option>
|
||||||
|
{% for user in team_members %}
|
||||||
|
<option value="{{ user.id }}">{{ user.username }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subtask-estimated-hours">Estimated Hours</label>
|
||||||
|
<input type="number" id="subtask-estimated-hours" name="estimated_hours" step="0.5" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subtask-start-date">Start Date</label>
|
||||||
|
<input type="date" id="subtask-start-date" name="start_date">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="subtask-due-date">Due Date</label>
|
||||||
|
<input type="date" id="subtask-due-date" name="due_date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Save Subtask</button>
|
||||||
|
<button type="button" class="btn btn-secondary" id="cancel-subtask">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.task-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-info h2 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-description {
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h3 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks-container {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-card:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-header {
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-title-area {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-name {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-description {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-details {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-detail {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: #4CAF50;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-not-started { background: #f8d7da; color: #721c24; }
|
||||||
|
.status-in-progress { background: #d1ecf1; color: #0c5460; }
|
||||||
|
.status-on-hold { background: #fff3cd; color: #856404; }
|
||||||
|
.status-completed { background: #d4edda; color: #155724; }
|
||||||
|
.status-cancelled { background: #f8d7da; color: #721c24; }
|
||||||
|
|
||||||
|
.priority-badge {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priority-low { background: #e2e3e5; color: #383d41; }
|
||||||
|
.priority-medium { background: #b8daff; color: #004085; }
|
||||||
|
.priority-high { background: #ffeaa7; color: #856404; }
|
||||||
|
.priority-urgent { background: #f5c6cb; color: #721c24; }
|
||||||
|
|
||||||
|
.subtasks-section {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtasks-section h5 {
|
||||||
|
margin: 0 0 0.75rem 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtasks-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtask-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtask-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtask-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtask-assignee {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtask-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tasks {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tasks-content {
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tasks h3 {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-tasks p {
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-details {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const taskModal = document.getElementById('task-modal');
|
||||||
|
const subtaskModal = document.getElementById('subtask-modal');
|
||||||
|
const taskForm = document.getElementById('task-form');
|
||||||
|
const subtaskForm = document.getElementById('subtask-form');
|
||||||
|
|
||||||
|
// Task Modal Functions
|
||||||
|
function openTaskModal(taskData = null) {
|
||||||
|
const isEdit = taskData !== null;
|
||||||
|
document.getElementById('task-modal-title').textContent = isEdit ? 'Edit Task' : 'Create Task';
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
document.getElementById('task-id').value = taskData.id;
|
||||||
|
document.getElementById('task-name').value = taskData.name;
|
||||||
|
document.getElementById('task-description').value = taskData.description || '';
|
||||||
|
document.getElementById('task-status').value = taskData.status;
|
||||||
|
document.getElementById('task-priority').value = taskData.priority;
|
||||||
|
document.getElementById('task-assigned-to').value = taskData.assigned_to_id || '';
|
||||||
|
document.getElementById('task-estimated-hours').value = taskData.estimated_hours || '';
|
||||||
|
document.getElementById('task-start-date').value = taskData.start_date || '';
|
||||||
|
document.getElementById('task-due-date').value = taskData.due_date || '';
|
||||||
|
} else {
|
||||||
|
taskForm.reset();
|
||||||
|
document.getElementById('task-id').value = '';
|
||||||
|
document.getElementById('task-priority').value = 'Medium';
|
||||||
|
}
|
||||||
|
|
||||||
|
taskModal.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSubtaskModal(taskId, subtaskData = null) {
|
||||||
|
const isEdit = subtaskData !== null;
|
||||||
|
document.getElementById('subtask-modal-title').textContent = isEdit ? 'Edit Subtask' : 'Create Subtask';
|
||||||
|
document.getElementById('parent-task-id').value = taskId;
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
document.getElementById('subtask-id').value = subtaskData.id;
|
||||||
|
document.getElementById('subtask-name').value = subtaskData.name;
|
||||||
|
document.getElementById('subtask-description').value = subtaskData.description || '';
|
||||||
|
document.getElementById('subtask-status').value = subtaskData.status;
|
||||||
|
document.getElementById('subtask-priority').value = subtaskData.priority;
|
||||||
|
document.getElementById('subtask-assigned-to').value = subtaskData.assigned_to_id || '';
|
||||||
|
document.getElementById('subtask-estimated-hours').value = subtaskData.estimated_hours || '';
|
||||||
|
document.getElementById('subtask-start-date').value = subtaskData.start_date || '';
|
||||||
|
document.getElementById('subtask-due-date').value = subtaskData.due_date || '';
|
||||||
|
} else {
|
||||||
|
subtaskForm.reset();
|
||||||
|
document.getElementById('subtask-id').value = '';
|
||||||
|
document.getElementById('subtask-priority').value = 'Medium';
|
||||||
|
}
|
||||||
|
|
||||||
|
subtaskModal.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
document.getElementById('create-task-btn').addEventListener('click', () => openTaskModal());
|
||||||
|
document.getElementById('create-first-task-btn')?.addEventListener('click', () => openTaskModal());
|
||||||
|
|
||||||
|
// Task Actions
|
||||||
|
document.querySelectorAll('.edit-task-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const taskId = this.getAttribute('data-id');
|
||||||
|
// Fetch task data and open modal (implement API call)
|
||||||
|
fetch(`/api/tasks/${taskId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
openTaskModal(data.task);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.add-subtask-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const taskId = this.getAttribute('data-id');
|
||||||
|
openSubtaskModal(taskId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.delete-task-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const taskId = this.getAttribute('data-id');
|
||||||
|
if (confirm('Are you sure you want to delete this task? All subtasks will also be deleted.')) {
|
||||||
|
fetch(`/api/tasks/${taskId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subtask Actions
|
||||||
|
document.querySelectorAll('.edit-subtask-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const subtaskId = this.getAttribute('data-id');
|
||||||
|
const taskId = this.closest('.task-card').getAttribute('data-task-id');
|
||||||
|
// Fetch subtask data and open modal
|
||||||
|
fetch(`/api/subtasks/${subtaskId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
openSubtaskModal(taskId, data.subtask);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.delete-subtask-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
const subtaskId = this.getAttribute('data-id');
|
||||||
|
if (confirm('Are you sure you want to delete this subtask?')) {
|
||||||
|
fetch(`/api/subtasks/${subtaskId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form Submissions
|
||||||
|
taskForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const taskId = formData.get('task_id');
|
||||||
|
const isEdit = taskId !== '';
|
||||||
|
|
||||||
|
const url = isEdit ? `/api/tasks/${taskId}` : '/api/tasks';
|
||||||
|
const method = isEdit ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(Object.fromEntries(formData))
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
taskModal.style.display = 'none';
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
subtaskForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const subtaskId = formData.get('subtask_id');
|
||||||
|
const isEdit = subtaskId !== '';
|
||||||
|
|
||||||
|
const url = isEdit ? `/api/subtasks/${subtaskId}` : '/api/subtasks';
|
||||||
|
const method = isEdit ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(Object.fromEntries(formData))
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
subtaskModal.style.display = 'none';
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + data.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal Close Events
|
||||||
|
document.querySelectorAll('.close').forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
this.closest('.modal').style.display = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('cancel-task').addEventListener('click', () => {
|
||||||
|
taskModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('cancel-subtask').addEventListener('click', () => {
|
||||||
|
subtaskModal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modals when clicking outside
|
||||||
|
window.addEventListener('click', function(event) {
|
||||||
|
if (event.target === taskModal) {
|
||||||
|
taskModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
if (event.target === subtaskModal) {
|
||||||
|
subtaskModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user