Remove obsolete Kanban parts.

This commit is contained in:
2025-07-04 21:55:54 +02:00
parent 43b99a0c3e
commit 1fe3f18bbd
11 changed files with 625 additions and 3437 deletions

View File

@@ -93,7 +93,6 @@
<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?')">

View File

@@ -169,32 +169,6 @@
</div>
{% endif %}
<!-- Kanban Boards -->
{% if kanban_boards %}
<div class="table-section">
<h3>📋 Kanban Boards ({{ kanban_boards|length }})</h3>
<table class="data-table">
<thead>
<tr>
<th>Board Name</th>
<th>Columns</th>
<th>Cards</th>
<th>Created By</th>
</tr>
</thead>
<tbody>
{% for board in kanban_boards %}
<tr>
<td>{{ board.name }}</td>
<td>{{ board.columns|length }}</td>
<td>{{ board.columns|map(attribute='cards')|map('length')|sum }}</td>
<td>{{ board.created_by.username }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<!-- Time Entries -->
{% if time_entries_count > 0 %}

View File

@@ -116,15 +116,6 @@
</div>
</div>
</div>
<div class="widget-option" data-type="kanban_summary">
<div class="widget-preview">
<i class="fas fa-th-large"></i>
<div class="widget-info">
<h5>Kanban Summary</h5>
<p>Mini kanban board for quick task management</p>
</div>
</div>
</div>
</div>
</div>
@@ -535,33 +526,6 @@
color: #333;
}
/* Kanban Summary Widget */
.kanban-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 0.5rem;
padding: 0.5rem;
}
.kanban-column-summary {
text-align: center;
padding: 0.5rem;
background: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #007bff;
}
.column-name {
font-size: 0.8rem;
color: #666;
margin-bottom: 0.25rem;
}
.column-count {
font-size: 1.2rem;
font-weight: 600;
color: #333;
}
/* Project Progress Widget */
.project-progress-list {
@@ -887,7 +851,6 @@ const widgetSizes = {
'break_reminder': 'large',
'project_progress': 'wide',
'task_priority': 'medium',
'kanban_summary': 'large',
'productivity_metrics': 'medium',
'time_distribution': 'wide',
'team_activity': 'wide',
@@ -1034,8 +997,6 @@ function renderWidgetContent(widget) {
return renderAssignedTasksWidget(widget);
case 'task_priority':
return renderTaskPriorityWidget(widget);
case 'kanban_summary':
return renderKanbanSummaryWidget(widget);
case 'productivity_metrics':
return renderProductivityMetricsWidget(widget);
case 'time_distribution':
@@ -1154,14 +1115,6 @@ function renderTaskPriorityWidget(widget) {
`;
}
function renderKanbanSummaryWidget(widget) {
loadWidgetData(widget.id);
return `
<div class="kanban-summary-widget" id="widget-content-${widget.id}">
<div class="widget-loading">Loading kanban...</div>
</div>
`;
}
function renderProductivityMetricsWidget(widget) {
loadWidgetData(widget.id);
@@ -1294,7 +1247,6 @@ function showConfigurationModal() {
'project_progress': 'Project Progress',
'assigned_tasks': 'Assigned Tasks',
'task_priority': 'Task Priority',
'kanban_summary': 'Kanban Summary',
'productivity_metrics': 'Productivity Metrics',
'time_distribution': 'Time Distribution'
};
@@ -1536,9 +1488,6 @@ function updateWidgetContent(widgetId, data) {
} else if (data.priority_tasks) {
// Update task priority widget
updateTaskPriorityWidget(widgetId, data.priority_tasks);
} else if (data.kanban_boards) {
// Update kanban summary widget
updateKanbanSummaryWidget(widgetId, data.kanban_boards);
} else if (data.project_progress) {
// Update project progress widget
updateProjectProgressWidget(widgetId, data.project_progress);
@@ -1698,26 +1647,6 @@ function updateTaskPriorityWidget(widgetId, priorityTasks) {
contentElement.innerHTML = tasksHtml;
}
function updateKanbanSummaryWidget(widgetId, kanbanBoards) {
const contentElement = document.getElementById(`widget-content-${widgetId}`);
if (!contentElement) return;
let kanbanHtml = '<div class="kanban-summary-list">';
kanbanBoards.forEach(board => {
kanbanHtml += `
<div class="kanban-board-item">
<div class="board-name">${board.name}</div>
<div class="board-project">${board.project_name}</div>
<div class="board-stats">
<span class="stat">${board.total_cards} cards</span>
<span class="stat">${board.columns} columns</span>
</div>
</div>
`;
});
kanbanHtml += '</div>';
contentElement.innerHTML = kanbanHtml;
}
function updateProjectProgressWidget(widgetId, projectProgress) {
const contentElement = document.getElementById(`widget-content-${widgetId}`);

View File

@@ -1,695 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<div class="page-container kanban-overview-container">
<div class="page-header overview-header">
<h2>Unified Kanban Boards</h2>
<p class="overview-description">Organize tasks from any project on shared company-wide boards</p>
</div>
{% if create_board %}
<!-- Create Board Form -->
<div class="create-board-form">
<div class="form-header">
<h3>Create New Kanban Board</h3>
<button type="button" id="cancel-create-board" class="btn btn-secondary">Cancel</button>
</div>
<form id="create-board-form" class="board-form">
<div class="form-group">
<label for="board-name">Board Name <span class="required">*</span></label>
<input type="text" id="board-name" name="name" required placeholder="Enter board name">
</div>
<div class="form-group">
<label for="board-description">Description</label>
<textarea id="board-description" name="description" placeholder="Optional description"></textarea>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="board-is-default" name="is_default">
Set as default board
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create Board</button>
</div>
</form>
</div>
{% endif %}
{% if boards %}
<div class="boards-grid">
{% for board in boards %}
<div class="card board-card">
<div class="card-header board-header">
<div class="board-info">
<h3 class="board-name">
{{ board.name }}
{% if board.is_default %}
<span class="default-badge">Default</span>
{% endif %}
</h3>
{% if board.description %}
<p class="board-description">{{ board.description }}</p>
{% endif %}
</div>
<div class="board-actions">
<a href="{{ url_for('kanban_overview') }}?board={{ board.id }}" class="btn btn-primary">Open Board</a>
{% if g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator', 'System Administrator'] %}
<button class="btn btn-secondary" onclick="editBoard({{ board.id }})">Edit</button>
{% endif %}
</div>
</div>
<div class="board-stats-section">
<div class="stat-grid">
<div class="stat-item">
<span class="stat-number">{{ board.columns|length }}</span>
<span class="stat-label">Columns</span>
</div>
<div class="stat-item">
{% set board_cards = 0 %}
{% for column in board.columns %}
{% set board_cards = board_cards + column.cards|selectattr('is_active')|list|length %}
{% endfor %}
<span class="stat-number">{{ board_cards }}</span>
<span class="stat-label">Cards</span>
</div>
<div class="stat-item">
{% set project_contexts = [] %}
{% for column in board.columns %}
{% for card in column.cards %}
{% if card.is_active and card.project_id and card.project_id not in project_contexts %}
{% set _ = project_contexts.append(card.project_id) %}
{% endif %}
{% endfor %}
{% endfor %}
<span class="stat-number">{{ project_contexts|length }}</span>
<span class="stat-label">Projects</span>
</div>
</div>
</div>
<!-- Quick Board Preview -->
<div class="board-preview">
{% if board.columns %}
<h5>Board Preview</h5>
<div class="preview-columns">
{% for column in board.columns %}
{% if loop.index <= 4 %}
<div class="preview-column" style="border-top: 3px solid {{ column.color }};">
<div class="preview-column-header">
<span class="preview-column-name">{{ column.name }}</span>
<span class="preview-card-count">{{ column.cards|selectattr('is_active')|list|length }}</span>
</div>
<div class="preview-cards">
{% set active_cards = column.cards|selectattr('is_active')|list %}
{% for card in active_cards %}
{% if loop.index <= 3 %}
<div class="preview-card" {% if card.color %}style="background-color: {{ card.color }};"{% endif %}>
<div class="preview-card-title">
{% if card.project %}
<span class="preview-card-project">[{{ card.project.code }}]</span>
{% endif %}
{% if card.title|length > 25 %}
{{ card.title|truncate(25, True) }}
{% else %}
{{ card.title }}
{% endif %}
</div>
{% if card.assigned_to %}
<div class="preview-card-assignee">{{ card.assigned_to.username }}</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
{% if active_cards|length > 3 %}
<div class="preview-more">+{{ active_cards|length - 3 }} more</div>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
{% if board.columns|length > 4 %}
<div class="preview-more-columns">
+{{ board.columns|length - 4 }} more columns
</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<!-- Quick Stats -->
<div class="quick-stats">
<div class="stat-card">
<h3>{{ boards|length }}</h3>
<p>Company Boards</p>
</div>
<div class="stat-card">
{% set total_cards = 0 %}
{% for board in boards %}
{% for column in board.columns %}
{% set total_cards = total_cards + column.cards|selectattr('is_active')|list|length %}
{% endfor %}
{% endfor %}
<h3>{{ total_cards }}</h3>
<p>Total Cards</p>
</div>
<div class="stat-card">
{% set total_projects = [] %}
{% for board in boards %}
{% for column in board.columns %}
{% for card in column.cards %}
{% if card.is_active and card.project_id and card.project_id not in total_projects %}
{% set _ = total_projects.append(card.project_id) %}
{% endif %}
{% endfor %}
{% endfor %}
{% endfor %}
<h3>{{ total_projects|length }}</h3>
<p>Active Projects</p>
</div>
</div>
{% else %}
<!-- No Kanban Boards -->
<div class="no-kanban">
<div class="no-kanban-content">
<div class="no-kanban-icon">📋</div>
<h3>No Kanban Boards Yet</h3>
<p>Create unified boards to organize tasks from any project.</p>
<div class="getting-started">
<h4>Getting Started:</h4>
<ol>
<li>Click the <strong>"Create Board"</strong> button</li>
<li>Set up columns for your workflow (To Do, In Progress, Done)</li>
<li>Add cards from any project to organize your work</li>
</ol>
</div>
{% if g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator', 'System Administrator'] %}
<button id="create-first-unified-board" class="btn btn-primary">Create First Board</button>
{% endif %}
</div>
</div>
{% endif %}
</div>
<style>
/* Container and header styles now inherited from common styles.css */
.overview-header {
text-align: center;
}
.overview-description {
color: #666;
margin: 0;
}
.boards-grid {
display: grid;
gap: 2rem;
margin-bottom: 2rem;
}
.board-card {
/* Card styling now inherited from common .card class */
border-radius: 12px;
}
.board-header {
padding: 1.5rem;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.board-info {
flex: 1;
}
.board-name {
margin: 0 0 0.5rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.2rem;
font-weight: 600;
color: #333;
}
.board-description {
color: #666;
margin: 0.5rem 0 0 0;
font-size: 0.9rem;
}
.board-actions {
display: flex;
gap: 0.5rem;
align-items: flex-start;
}
.board-stats-section {
padding: 1.5rem;
border-bottom: 1px solid #dee2e6;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 1rem;
}
.stat-item {
text-align: center;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.stat-number {
display: block;
font-size: 1.5rem;
font-weight: 700;
color: #007bff;
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.8rem;
color: #666;
font-weight: 500;
}
.boards-section h4 {
margin: 0 0 1rem 0;
color: #333;
}
.boards-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.board-item {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.board-item:hover {
background: #e7f3ff;
border-color: #007bff;
transform: translateY(-1px);
}
.board-info {
flex: 1;
}
.board-name {
font-weight: 600;
margin-bottom: 0.25rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.default-badge {
background: #28a745;
color: white;
padding: 0.15rem 0.4rem;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 500;
}
.board-description {
color: #666;
font-size: 0.9rem;
}
.board-stats {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.8rem;
color: #666;
text-align: right;
}
.board-preview {
padding: 1.5rem;
}
.board-preview h5 {
margin: 0 0 1rem 0;
color: #333;
}
.preview-columns {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
}
.preview-column {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
overflow: hidden;
}
.preview-column-header {
padding: 0.75rem;
background: white;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
}
.preview-column-name {
font-weight: 600;
font-size: 0.9rem;
}
.preview-card-count {
background: #e9ecef;
padding: 0.2rem 0.4rem;
border-radius: 8px;
font-size: 0.7rem;
font-weight: 500;
}
.preview-cards {
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.preview-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 0.5rem;
font-size: 0.8rem;
}
.preview-card-title {
font-weight: 500;
margin-bottom: 0.25rem;
}
.preview-card-project {
background: #f3e8ff;
color: #7c3aed;
padding: 0.1rem 0.3rem;
border-radius: 3px;
font-size: 0.6rem;
font-weight: 600;
margin-right: 0.25rem;
}
.preview-card-assignee {
color: #666;
font-size: 0.7rem;
}
.preview-more {
color: #666;
font-size: 0.8rem;
text-align: center;
padding: 0.25rem;
}
.preview-more-columns {
display: flex;
align-items: center;
justify-content: center;
color: #666;
font-size: 0.9rem;
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 1rem;
}
.quick-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 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: #007bff;
}
.stat-card p {
margin: 0;
color: #666;
font-weight: 500;
}
.no-kanban {
text-align: center;
padding: 4rem 2rem;
}
.no-kanban-content {
max-width: 500px;
margin: 0 auto;
}
.no-kanban-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.no-kanban h3 {
color: #666;
margin-bottom: 1rem;
}
.no-kanban p {
color: #999;
margin-bottom: 2rem;
}
.getting-started {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
text-align: left;
}
.getting-started h4 {
margin: 0 0 1rem 0;
color: #333;
}
.getting-started ol {
margin: 0;
padding-left: 1.5rem;
}
.getting-started li {
margin-bottom: 0.5rem;
color: #666;
}
.create-board-form {
background: white;
border: 1px solid #dee2e6;
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.form-header h3 {
margin: 0;
color: #333;
}
.board-form {
max-width: 600px;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #333;
}
.form-group input[type="text"],
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #dee2e6;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s ease;
}
.form-group input[type="text"]:focus,
.form-group textarea:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0,123,255,0.25);
}
.form-group textarea {
height: 80px;
resize: vertical;
}
.form-group input[type="checkbox"] {
margin-right: 0.5rem;
}
.form-actions {
margin-top: 1.5rem;
}
.required {
color: #dc3545;
}
.category-badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
@media (max-width: 768px) {
.board-header {
flex-direction: column;
gap: 1rem;
}
.board-actions {
width: 100%;
justify-content: flex-start;
}
.preview-columns {
grid-template-columns: 1fr;
}
.quick-stats {
grid-template-columns: 1fr;
}
.stat-grid {
grid-template-columns: repeat(3, 1fr);
}
}
</style>
<script>
function openBoard(boardId) {
window.location.href = `/kanban?board=${boardId}`;
}
function editBoard(boardId) {
// Implement board editing functionality
alert('Board editing functionality to be implemented');
}
// Handle create first board button
document.addEventListener('DOMContentLoaded', function() {
const createFirstBoardBtn = document.getElementById('create-first-unified-board');
if (createFirstBoardBtn) {
createFirstBoardBtn.addEventListener('click', function() {
// Redirect to a page where they can create a board
window.location.href = '/kanban?create=true';
});
}
// Handle cancel create board
const cancelCreateBoardBtn = document.getElementById('cancel-create-board');
if (cancelCreateBoardBtn) {
cancelCreateBoardBtn.addEventListener('click', function() {
window.location.href = '/kanban';
});
}
// Handle create board form submission
const createBoardForm = document.getElementById('create-board-form');
if (createBoardForm) {
createBoardForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(createBoardForm);
const data = {
name: formData.get('name'),
description: formData.get('description'),
is_default: formData.get('is_default') === 'on'
};
fetch('/api/kanban/boards', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Redirect to the kanban overview page
window.location.href = '/kanban';
} else {
alert('Error creating board: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error creating board. Please try again.');
});
});
}
});
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -109,11 +109,21 @@
<div class="form-row">
<div class="form-group">
<label for="sprint-start-date">Start Date *</label>
<input type="date" id="sprint-start-date" required>
<div class="hybrid-date-input">
<input type="date" id="sprint-start-date-native" class="date-input-native" required>
<input type="text" id="sprint-start-date" class="date-input-formatted" required placeholder="{{ "YYYY-MM-DD" if (g.user.preferences.date_format if g.user.preferences else "ISO") == "ISO" else "MM/DD/YYYY" if (g.user.preferences.date_format if g.user.preferences else "ISO") == "US" else "DD/MM/YYYY" if (g.user.preferences.date_format if g.user.preferences else "ISO") in ["EU", "UK"] else "Mon, Dec 25, 2024" }}">
<button type="button" class="calendar-picker-btn" onclick="openCalendarPicker('sprint-start-date')" title="Open calendar">📅</button>
</div>
<div class="date-error" id="sprint-start-date-error" style="display: none; color: #dc3545; font-size: 0.8rem; margin-top: 0.25rem;"></div>
</div>
<div class="form-group">
<label for="sprint-end-date">End Date *</label>
<input type="date" id="sprint-end-date" required>
<div class="hybrid-date-input">
<input type="date" id="sprint-end-date-native" class="date-input-native" required>
<input type="text" id="sprint-end-date" class="date-input-formatted" required placeholder="{{ "YYYY-MM-DD" if (g.user.preferences.date_format if g.user.preferences else "ISO") == "ISO" else "MM/DD/YYYY" if (g.user.preferences.date_format if g.user.preferences else "ISO") == "US" else "DD/MM/YYYY" if (g.user.preferences.date_format if g.user.preferences else "ISO") in ["EU", "UK"] else "Mon, Dec 25, 2024" }}">
<button type="button" class="calendar-picker-btn" onclick="openCalendarPicker('sprint-end-date')" title="Open calendar">📅</button>
</div>
<div class="date-error" id="sprint-end-date-error" style="display: none; color: #dc3545; font-size: 0.8rem; margin-top: 0.25rem;"></div>
</div>
</div>
@@ -227,9 +237,6 @@
margin-bottom: 2rem;
}
.sprint-card {
/* Sprint card inherits from .management-card */
}
.sprint-card-header {
display: flex;
@@ -294,6 +301,69 @@
/* Hybrid Date Input Styles */
.hybrid-date-input {
position: relative;
display: flex;
align-items: center;
gap: 0.25rem;
}
.hybrid-date-input.compact {
display: inline-flex;
}
.date-input-native {
position: absolute;
left: 0;
top: 0;
width: calc(100% - 35px); /* Leave space for calendar button */
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 2;
pointer-events: auto;
}
.date-input-formatted {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
background: white;
position: relative;
z-index: 2;
}
.calendar-picker-btn {
background: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
padding: 0.5rem;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
z-index: 3;
position: relative;
}
.calendar-picker-btn:hover {
background: #e9ecef;
}
.calendar-picker-btn.compact {
padding: 0.375rem 0.5rem;
font-size: 12px;
}
.hybrid-date-input.compact .date-input-formatted {
padding: 0.375rem;
font-size: 12px;
width: 100px;
flex: none;
}
@media (max-width: 768px) {
.sprint-metrics {
grid-template-columns: repeat(2, 1fr);
@@ -302,6 +372,206 @@
</style>
<script>
// User preferences for date formatting
const USER_DATE_FORMAT = '{{ g.user.preferences.date_format if g.user.preferences else "ISO" }}';
// Date formatting utility function
function formatUserDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
if (isNaN(date.getTime())) return '';
switch (USER_DATE_FORMAT) {
case 'US':
return date.toLocaleDateString('en-US'); // MM/DD/YYYY
case 'EU':
case 'UK':
return date.toLocaleDateString('en-GB'); // DD/MM/YYYY
case 'Readable':
return date.toLocaleDateString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric'
}); // Mon, Dec 25, 2024
case 'ISO':
default:
return date.toISOString().split('T')[0]; // YYYY-MM-DD
}
}
// Date input formatting function - formats ISO date for user input
function formatDateForInput(isoDateString) {
if (!isoDateString) return '';
const date = new Date(isoDateString);
if (isNaN(date.getTime())) return '';
return formatUserDate(isoDateString);
}
// Date parsing function - converts user-formatted date to ISO format
function parseUserDate(dateString) {
if (!dateString || dateString.trim() === '') return null;
const trimmed = dateString.trim();
let date;
switch (USER_DATE_FORMAT) {
case 'US': // MM/DD/YYYY
const usParts = trimmed.split('/');
if (usParts.length === 3) {
const month = parseInt(usParts[0], 10);
const day = parseInt(usParts[1], 10);
const year = parseInt(usParts[2], 10);
if (month >= 1 && month <= 12 && day >= 1 && day <= 31 && year > 1900) {
date = new Date(year, month - 1, day);
}
}
break;
case 'EU':
case 'UK': // DD/MM/YYYY
const euParts = trimmed.split('/');
if (euParts.length === 3) {
const day = parseInt(euParts[0], 10);
const month = parseInt(euParts[1], 10);
const year = parseInt(euParts[2], 10);
if (month >= 1 && month <= 12 && day >= 1 && day <= 31 && year > 1900) {
date = new Date(year, month - 1, day);
}
}
break;
case 'Readable': // Mon, Dec 25, 2024
date = new Date(trimmed);
break;
case 'ISO': // YYYY-MM-DD
default:
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
date = new Date(trimmed);
}
break;
}
if (!date || isNaN(date.getTime())) {
return null;
}
return date.toISOString().split('T')[0];
}
// Date validation function
function validateDateInput(inputElement, errorElement) {
const value = inputElement.value.trim();
if (!value) {
errorElement.style.display = 'none';
return true;
}
const parsedDate = parseUserDate(value);
if (!parsedDate) {
let expectedFormat;
switch (USER_DATE_FORMAT) {
case 'US': expectedFormat = 'MM/DD/YYYY'; break;
case 'EU':
case 'UK': expectedFormat = 'DD/MM/YYYY'; break;
case 'Readable': expectedFormat = 'Mon, Dec 25, 2024'; break;
case 'ISO':
default: expectedFormat = 'YYYY-MM-DD'; break;
}
errorElement.textContent = `Invalid date format. Expected: ${expectedFormat}`;
errorElement.style.display = 'block';
return false;
}
errorElement.style.display = 'none';
return true;
}
// Date range validation function
function validateDateRange(startElement, endElement, startErrorElement, endErrorElement) {
const startValid = validateDateInput(startElement, startErrorElement);
const endValid = validateDateInput(endElement, endErrorElement);
if (!startValid || !endValid) {
return false;
}
const startDate = parseUserDate(startElement.value);
const endDate = parseUserDate(endElement.value);
if (startDate && endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
if (start >= end) {
endErrorElement.textContent = 'End date must be after start date';
endErrorElement.style.display = 'block';
return false;
}
}
return true;
}
// Hybrid Date Input Functions
function setupHybridDateInput(inputId) {
const formattedInput = document.getElementById(inputId);
const nativeInput = document.getElementById(inputId + '-native');
if (!formattedInput || !nativeInput) return;
// Sync from native input to formatted input
nativeInput.addEventListener('change', function() {
if (this.value) {
formattedInput.value = formatDateForInput(this.value);
// Trigger change event on formatted input
formattedInput.dispatchEvent(new Event('change'));
}
});
// Sync from formatted input to native input
formattedInput.addEventListener('change', function() {
const isoDate = parseUserDate(this.value);
if (isoDate) {
nativeInput.value = isoDate;
} else {
nativeInput.value = '';
}
});
// Clear both inputs when formatted input is cleared
formattedInput.addEventListener('input', function() {
if (this.value === '') {
nativeInput.value = '';
}
});
}
function openCalendarPicker(inputId) {
const nativeInput = document.getElementById(inputId + '-native');
if (nativeInput) {
// Try multiple methods to open the date picker
nativeInput.focus();
// For modern browsers
if (nativeInput.showPicker) {
try {
nativeInput.showPicker();
} catch (e) {
// Fallback to click if showPicker fails
nativeInput.click();
}
} else {
// Fallback for older browsers
nativeInput.click();
}
}
}
// Sprint Management Controller
class SprintManager {
constructor() {
@@ -331,6 +601,27 @@ class SprintManager {
document.getElementById('refresh-sprints').addEventListener('click', () => {
this.loadSprints();
});
// Date validation
document.getElementById('sprint-start-date').addEventListener('blur', () => {
const startInput = document.getElementById('sprint-start-date');
const endInput = document.getElementById('sprint-end-date');
const startError = document.getElementById('sprint-start-date-error');
const endError = document.getElementById('sprint-end-date-error');
validateDateRange(startInput, endInput, startError, endError);
});
document.getElementById('sprint-end-date').addEventListener('blur', () => {
const startInput = document.getElementById('sprint-start-date');
const endInput = document.getElementById('sprint-end-date');
const startError = document.getElementById('sprint-start-date-error');
const endError = document.getElementById('sprint-end-date-error');
validateDateRange(startInput, endInput, startError, endError);
});
// Setup hybrid date inputs
setupHybridDateInput('sprint-start-date');
setupHybridDateInput('sprint-end-date');
// Modal close handlers
document.querySelectorAll('.close').forEach(closeBtn => {
@@ -440,7 +731,7 @@ class SprintManager {
</div>
<div class="sprint-dates">
📅 ${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}
📅 ${formatUserDate(sprint.start_date)} - ${formatUserDate(sprint.end_date)}
${sprint.days_remaining > 0 ? `(${sprint.days_remaining} days left)` : ''}
</div>
@@ -500,8 +791,8 @@ class SprintManager {
document.getElementById('sprint-status').value = sprint.status;
document.getElementById('sprint-project').value = sprint.project_id || '';
document.getElementById('sprint-capacity').value = sprint.capacity_hours || '';
document.getElementById('sprint-start-date').value = sprint.start_date;
document.getElementById('sprint-end-date').value = sprint.end_date;
document.getElementById('sprint-start-date').value = formatDateForInput(sprint.start_date);
document.getElementById('sprint-end-date').value = formatDateForInput(sprint.end_date);
document.getElementById('sprint-goal').value = sprint.goal || '';
document.getElementById('delete-sprint-btn').style.display = 'inline-block';
} else {
@@ -512,8 +803,8 @@ class SprintManager {
// Set default dates (next 2 weeks)
const today = new Date();
const twoWeeksLater = new Date(today.getTime() + 14 * 24 * 60 * 60 * 1000);
document.getElementById('sprint-start-date').value = today.toISOString().split('T')[0];
document.getElementById('sprint-end-date').value = twoWeeksLater.toISOString().split('T')[0];
document.getElementById('sprint-start-date').value = formatDateForInput(today.toISOString().split('T')[0]);
document.getElementById('sprint-end-date').value = formatDateForInput(twoWeeksLater.toISOString().split('T')[0]);
document.getElementById('delete-sprint-btn').style.display = 'none';
}
@@ -522,14 +813,29 @@ class SprintManager {
}
async saveSprint() {
// Validate date inputs before saving
const startInput = document.getElementById('sprint-start-date');
const endInput = document.getElementById('sprint-end-date');
const startError = document.getElementById('sprint-start-date-error');
const endError = document.getElementById('sprint-end-date-error');
if (!validateDateRange(startInput, endInput, startError, endError)) {
if (startError.style.display !== 'none') {
startInput.focus();
} else {
endInput.focus();
}
return;
}
const sprintData = {
name: document.getElementById('sprint-name').value,
description: document.getElementById('sprint-description').value,
status: document.getElementById('sprint-status').value,
project_id: document.getElementById('sprint-project').value || null,
capacity_hours: document.getElementById('sprint-capacity').value || null,
start_date: document.getElementById('sprint-start-date').value,
end_date: document.getElementById('sprint-end-date').value,
start_date: parseUserDate(document.getElementById('sprint-start-date').value),
end_date: parseUserDate(document.getElementById('sprint-end-date').value),
goal: document.getElementById('sprint-goal').value || null
};

View File

@@ -44,10 +44,18 @@
<!-- Date Range Filters -->
<div class="date-range-filters">
<label for="start-date-filter">From:</label>
<input type="date" id="start-date-filter" class="filter-input">
<div class="hybrid-date-input compact">
<input type="date" id="start-date-filter-native" class="date-input-native">
<input type="text" id="start-date-filter" class="date-input-formatted filter-input" placeholder="{{ "YYYY-MM-DD" if (g.user.preferences.date_format if g.user.preferences else "ISO") == "ISO" else "MM/DD/YYYY" if (g.user.preferences.date_format if g.user.preferences else "ISO") == "US" else "DD/MM/YYYY" if (g.user.preferences.date_format if g.user.preferences else "ISO") in ["EU", "UK"] else "Mon, Dec 25, 2024" }}">
<button type="button" class="calendar-picker-btn compact" onclick="openCalendarPicker('start-date-filter')" title="Open calendar">📅</button>
</div>
<label for="end-date-filter">To:</label>
<input type="date" id="end-date-filter" class="filter-input">
<div class="hybrid-date-input compact">
<input type="date" id="end-date-filter-native" class="date-input-native">
<input type="text" id="end-date-filter" class="date-input-formatted filter-input" placeholder="{{ "YYYY-MM-DD" if (g.user.preferences.date_format if g.user.preferences else "ISO") == "ISO" else "MM/DD/YYYY" if (g.user.preferences.date_format if g.user.preferences else "ISO") == "US" else "DD/MM/YYYY" if (g.user.preferences.date_format if g.user.preferences else "ISO") in ["EU", "UK"] else "Mon, Dec 25, 2024" }}">
<button type="button" class="calendar-picker-btn compact" onclick="openCalendarPicker('end-date-filter')" title="Open calendar">📅</button>
</div>
<select id="date-field-filter" class="filter-select">
<option value="created">Created Date</option>
@@ -93,9 +101,9 @@
</div>
</div>
<!-- Kanban Board -->
<div class="kanban-board" id="task-board">
<div class="kanban-column" data-status="NOT_STARTED">
<!-- Task Board -->
<div class="task-board" id="task-board">
<div class="task-column" data-status="NOT_STARTED">
<div class="column-header">
<h3>📝 Not Started</h3>
<span class="task-count">0</span>
@@ -105,7 +113,7 @@
</div>
</div>
<div class="kanban-column" data-status="IN_PROGRESS">
<div class="task-column" data-status="IN_PROGRESS">
<div class="column-header">
<h3>⚡ In Progress</h3>
<span class="task-count">0</span>
@@ -115,7 +123,7 @@
</div>
</div>
<div class="kanban-column" data-status="ON_HOLD">
<div class="task-column" data-status="ON_HOLD">
<div class="column-header">
<h3>⏸️ On Hold</h3>
<span class="task-count">0</span>
@@ -125,7 +133,7 @@
</div>
</div>
<div class="kanban-column" data-status="COMPLETED">
<div class="task-column" data-status="COMPLETED">
<div class="column-header">
<h3>✅ Completed</h3>
<span class="task-count">0</span>
@@ -217,11 +225,12 @@
<div class="form-row">
<div class="form-group">
<label for="task-due-date">Due Date</label>
<input type="date" id="task-due-date">
</div>
<div class="form-group">
<label for="task-estimated-hours">Estimated Hours</label>
<input type="number" id="task-estimated-hours" min="0" step="0.5">
<div class="hybrid-date-input">
<input type="date" id="task-due-date-native" class="date-input-native">
<input type="text" id="task-due-date" class="date-input-formatted" placeholder="{{ "YYYY-MM-DD" if (g.user.preferences.date_format if g.user.preferences else "ISO") == "ISO" else "MM/DD/YYYY" if (g.user.preferences.date_format if g.user.preferences else "ISO") == "US" else "DD/MM/YYYY" if (g.user.preferences.date_format if g.user.preferences else "ISO") in ["EU", "UK"] else "Mon, Dec 25, 2024" }}">
<button type="button" class="calendar-picker-btn" onclick="openCalendarPicker('task-due-date')" title="Open calendar">📅</button>
</div>
<div class="date-error" id="task-due-date-error" style="display: none; color: #dc3545; font-size: 0.8rem; margin-top: 0.25rem;"></div>
</div>
</div>
@@ -293,14 +302,77 @@
white-space: nowrap;
}
.kanban-board {
/* Hybrid Date Input Styles */
.hybrid-date-input {
position: relative;
display: flex;
align-items: center;
gap: 0.25rem;
}
.hybrid-date-input.compact {
display: inline-flex;
}
.date-input-native {
position: absolute;
left: 0;
top: 0;
width: calc(100% - 35px); /* Leave space for calendar button */
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 2;
pointer-events: auto;
}
.date-input-formatted {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
background: white;
position: relative;
z-index: 2;
}
.calendar-picker-btn {
background: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
padding: 0.5rem;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
z-index: 3;
position: relative;
}
.calendar-picker-btn:hover {
background: #e9ecef;
}
.calendar-picker-btn.compact {
padding: 0.375rem 0.5rem;
font-size: 12px;
}
.hybrid-date-input.compact .date-input-formatted {
padding: 0.375rem;
font-size: 12px;
width: 100px;
flex: none;
}
.task-board {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.kanban-column {
.task-column {
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
@@ -431,7 +503,7 @@
}
@media (max-width: 768px) {
.kanban-board {
.task-board {
grid-template-columns: 1fr;
}
}
@@ -441,6 +513,179 @@
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<script>
// User preferences for date formatting
const USER_DATE_FORMAT = '{{ g.user.preferences.date_format if g.user.preferences else "ISO" }}';
// Date formatting utility function
function formatUserDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
if (isNaN(date.getTime())) return '';
switch (USER_DATE_FORMAT) {
case 'US':
return date.toLocaleDateString('en-US'); // MM/DD/YYYY
case 'EU':
case 'UK':
return date.toLocaleDateString('en-GB'); // DD/MM/YYYY
case 'Readable':
return date.toLocaleDateString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric'
}); // Mon, Dec 25, 2024
case 'ISO':
default:
return date.toISOString().split('T')[0]; // YYYY-MM-DD
}
}
// Date input formatting function - formats ISO date for user input
function formatDateForInput(isoDateString) {
if (!isoDateString) return '';
const date = new Date(isoDateString);
if (isNaN(date.getTime())) return '';
return formatUserDate(isoDateString);
}
// Date parsing function - converts user-formatted date to ISO format
function parseUserDate(dateString) {
if (!dateString || dateString.trim() === '') return null;
const trimmed = dateString.trim();
let date;
switch (USER_DATE_FORMAT) {
case 'US': // MM/DD/YYYY
const usParts = trimmed.split('/');
if (usParts.length === 3) {
const month = parseInt(usParts[0], 10);
const day = parseInt(usParts[1], 10);
const year = parseInt(usParts[2], 10);
if (month >= 1 && month <= 12 && day >= 1 && day <= 31 && year > 1900) {
date = new Date(year, month - 1, day);
}
}
break;
case 'EU':
case 'UK': // DD/MM/YYYY
const euParts = trimmed.split('/');
if (euParts.length === 3) {
const day = parseInt(euParts[0], 10);
const month = parseInt(euParts[1], 10);
const year = parseInt(euParts[2], 10);
if (month >= 1 && month <= 12 && day >= 1 && day <= 31 && year > 1900) {
date = new Date(year, month - 1, day);
}
}
break;
case 'Readable': // Mon, Dec 25, 2024
date = new Date(trimmed);
break;
case 'ISO': // YYYY-MM-DD
default:
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
date = new Date(trimmed);
}
break;
}
if (!date || isNaN(date.getTime())) {
return null;
}
return date.toISOString().split('T')[0];
}
// Date validation function
function validateDateInput(inputElement, errorElement) {
const value = inputElement.value.trim();
if (!value) {
errorElement.style.display = 'none';
return true;
}
const parsedDate = parseUserDate(value);
if (!parsedDate) {
let expectedFormat;
switch (USER_DATE_FORMAT) {
case 'US': expectedFormat = 'MM/DD/YYYY'; break;
case 'EU':
case 'UK': expectedFormat = 'DD/MM/YYYY'; break;
case 'Readable': expectedFormat = 'Mon, Dec 25, 2024'; break;
case 'ISO':
default: expectedFormat = 'YYYY-MM-DD'; break;
}
errorElement.textContent = `Invalid date format. Expected: ${expectedFormat}`;
errorElement.style.display = 'block';
return false;
}
errorElement.style.display = 'none';
return true;
}
// Hybrid Date Input Functions
function setupHybridDateInput(inputId) {
const formattedInput = document.getElementById(inputId);
const nativeInput = document.getElementById(inputId + '-native');
if (!formattedInput || !nativeInput) return;
// Sync from native input to formatted input
nativeInput.addEventListener('change', function() {
if (this.value) {
formattedInput.value = formatDateForInput(this.value);
// Trigger change event on formatted input
formattedInput.dispatchEvent(new Event('change'));
}
});
// Sync from formatted input to native input
formattedInput.addEventListener('change', function() {
const isoDate = parseUserDate(this.value);
if (isoDate) {
nativeInput.value = isoDate;
} else {
nativeInput.value = '';
}
});
// Clear both inputs when formatted input is cleared
formattedInput.addEventListener('input', function() {
if (this.value === '') {
nativeInput.value = '';
}
});
}
function openCalendarPicker(inputId) {
const nativeInput = document.getElementById(inputId + '-native');
if (nativeInput) {
nativeInput.focus();
// Try showPicker() first for modern browsers
if (nativeInput.showPicker) {
try {
nativeInput.showPicker();
} catch (e) {
// Fallback to click if showPicker fails
nativeInput.click();
}
} else {
// Fallback for older browsers
nativeInput.click();
}
}
}
// Task Management Controller
class UnifiedTaskManager {
constructor() {
@@ -457,6 +702,7 @@ class UnifiedTaskManager {
};
this.currentTask = null;
this.sortableInstances = [];
this.currentUserId = {{ g.user.id|tojson }};
}
async init() {
@@ -492,12 +738,12 @@ class UnifiedTaskManager {
// Date range filters
document.getElementById('start-date-filter').addEventListener('change', () => {
this.filters.startDate = document.getElementById('start-date-filter').value;
this.filters.startDate = parseUserDate(document.getElementById('start-date-filter').value) || '';
this.applyFilters();
});
document.getElementById('end-date-filter').addEventListener('change', () => {
this.filters.endDate = document.getElementById('end-date-filter').value;
this.filters.endDate = parseUserDate(document.getElementById('end-date-filter').value) || '';
this.applyFilters();
});
@@ -520,6 +766,18 @@ class UnifiedTaskManager {
document.getElementById('refresh-tasks').addEventListener('click', () => {
this.loadTasks();
});
// Date validation
document.getElementById('task-due-date').addEventListener('blur', () => {
const input = document.getElementById('task-due-date');
const error = document.getElementById('task-due-date-error');
validateDateInput(input, error);
});
// Setup hybrid date inputs
setupHybridDateInput('task-due-date');
setupHybridDateInput('start-date-filter');
setupHybridDateInput('end-date-filter');
}
setupSortable() {
@@ -638,7 +896,7 @@ class UnifiedTaskManager {
getFilteredTasks() {
return this.tasks.filter(task => {
// View filter
if (this.currentView === 'personal' && task.assigned_to_id !== {{ g.user.id }}) {
if (this.currentView === 'personal' && task.assigned_to_id !== this.currentUserId) {
return false;
}
if (this.currentView === 'team' && !task.is_team_task) {
@@ -732,7 +990,7 @@ class UnifiedTaskManager {
${task.project_name ? `<div class="task-project">${task.project_code} - ${task.project_name}</div>` : ''}
<div class="task-meta">
<span class="task-assignee">${task.assigned_to_name || 'Unassigned'}</span>
${dueDate ? `<span class="task-due-date ${isOverdue ? 'overdue' : ''}">${dueDate.toLocaleDateString()}</span>` : ''}
${dueDate ? `<span class="task-due-date ${isOverdue ? 'overdue' : ''}">${formatUserDate(task.due_date)}</span>` : ''}
</div>
`;
@@ -801,7 +1059,7 @@ class UnifiedTaskManager {
document.getElementById('task-priority').value = task.priority;
document.getElementById('task-project').value = task.project_id || '';
document.getElementById('task-assignee').value = task.assigned_to_id || '';
document.getElementById('task-due-date').value = task.due_date || '';
document.getElementById('task-due-date').value = formatDateForInput(task.due_date) || '';
document.getElementById('task-estimated-hours').value = task.estimated_hours || '';
document.getElementById('task-status').value = task.status;
document.getElementById('delete-task-btn').style.display = 'inline-block';
@@ -816,13 +1074,22 @@ class UnifiedTaskManager {
}
async saveTask() {
// Validate date input before saving
const dueDateInput = document.getElementById('task-due-date');
const dueDateError = document.getElementById('task-due-date-error');
if (!validateDateInput(dueDateInput, dueDateError)) {
dueDateInput.focus();
return;
}
const taskData = {
name: document.getElementById('task-name').value,
description: document.getElementById('task-description').value,
priority: document.getElementById('task-priority').value,
project_id: document.getElementById('task-project').value || null,
assigned_to_id: document.getElementById('task-assignee').value || null,
due_date: document.getElementById('task-due-date').value || null,
due_date: parseUserDate(document.getElementById('task-due-date').value) || null,
estimated_hours: document.getElementById('task-estimated-hours').value || null,
status: document.getElementById('task-status').value
};