Remove obsolete Kanban parts.
This commit is contained in:
@@ -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?')">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user