Files
TimeTrack/templates/admin_projects.html
Jens Luedicke 5bb8ddfd61 Align button sizes.
CHANGE: Align button sizes across the Header/Title views.
2025-07-04 17:19:45 +02:00

624 lines
19 KiB
HTML

{% extends "layout.html" %}
{% block content %}
<div class="timetrack-container admin-projects-container">
<div class="admin-header">
<h2>Project Management</h2>
<div class="admin-actions">
<a href="{{ url_for('create_project') }}" class="btn btn-md btn-success">Create New Project</a>
<button id="manage-categories-btn" class="btn btn-md 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>
{% if projects %}
<div class="projects-table">
<table class="time-history">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Category</th>
<th>Team</th>
<th>Status</th>
<th>Start Date</th>
<th>End Date</th>
<th>Created By</th>
<th>Time Entries</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for project in projects %}
<tr class="{% if not project.is_active %}inactive-project{% endif %}">
<td><strong>{{ project.code }}</strong></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>
{% if project.team %}
{{ project.team.name }}
{% else %}
<em>All Teams</em>
{% endif %}
</td>
<td>
<span class="status-badge {% if project.is_active %}active{% else %}inactive{% endif %}">
{{ 'Active' if project.is_active else 'Inactive' }}
</span>
</td>
<td>{{ project.start_date.strftime('%Y-%m-%d') if project.start_date else '-' }}</td>
<td>{{ project.end_date.strftime('%Y-%m-%d') if project.end_date else '-' }}</td>
<td>{{ project.created_by.username }}</td>
<td>{{ project.time_entries|length }}</td>
<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('manage_project_tasks', project_id=project.id) }}" class="btn btn-sm btn-info">Tasks</a>
<a href="{{ url_for('project_kanban', project_id=project.id) }}" class="btn btn-sm btn-success">Kanban</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;"
onsubmit="return confirm('Are you sure you want to delete this project?')">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="no-data">
<p>No projects found. <a href="{{ url_for('create_project') }}">Create your first project</a>.</p>
</div>
{% endif %}
</div>
<style>
/* Override container width for admin projects page */
.admin-projects-container {
max-width: none !important;
width: 100% !important;
padding: 1rem !important;
margin: 0 !important;
}
.projects-table {
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 {
opacity: 0.6;
}
.status-badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 500;
}
.status-badge.active {
background-color: #d4edda;
color: #155724;
}
.status-badge.inactive {
background-color: #f8d7da;
color: #721c24;
}
.actions {
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 */
.no-data {
text-align: center;
padding: 3rem;
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>
<!-- Category Management Modal -->
<div id="category-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close">&times;</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 %}