Files
TimeTrack/templates/project_kanban.html

1377 lines
41 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "layout.html" %}
{% block content %}
<div class="kanban-container">
<div class="kanban-header">
<div class="project-info">
<h2>Kanban Board: {{ project.code }} - {{ project.name }}</h2>
<p class="project-description">{{ project.description or 'No description available' }}</p>
{% if project.category %}
<span class="category-badge" style="background-color: {{ project.category.color }}20; color: {{ project.category.color }};">
{{ project.category.icon or '📁' }} {{ project.category.name }}
</span>
{% endif %}
</div>
<div class="kanban-actions">
{% if g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator', 'System Administrator'] %}
<button id="create-board-btn" class="btn btn-md btn-success">Create Board</button>
<button id="manage-columns-btn" class="btn btn-md btn-warning" style="display: none;">Manage Columns</button>
{% endif %}
<a href="{{ url_for('admin_projects') }}" class="btn btn-md btn-secondary">Back to Projects</a>
<a href="{{ url_for('manage_project_tasks', project_id=project.id) }}" class="btn btn-md btn-info">Task View</a>
</div>
</div>
<!-- Board Selection -->
{% if boards %}
<div class="board-selection">
<label for="board-select">Select Board:</label>
<select id="board-select" class="form-control">
<option value="">Choose a board...</option>
{% for board in boards %}
<option value="{{ board.id }}" {% if board.is_default %}selected{% endif %}>
{{ board.name }} {% if board.is_default %}(Default){% endif %}
</option>
{% endfor %}
</select>
</div>
{% endif %}
<!-- Kanban Board -->
<div id="kanban-board" class="kanban-board" style="display: none;">
<div id="kanban-columns" class="kanban-columns">
<!-- Columns will be loaded dynamically -->
</div>
</div>
<!-- No Boards Message -->
{% if not boards %}
<div class="no-boards">
<div class="no-boards-content">
<h3>No Kanban Boards Created Yet</h3>
<p>Create your first Kanban board to start organizing tasks visually.</p>
{% if g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator', 'System Administrator'] %}
<button id="create-first-board-btn" class="btn btn-md btn-primary">Create Your First Board</button>
{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- Board Creation Modal -->
<div id="board-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close">&times;</span>
<h3>Create Kanban Board</h3>
<form id="board-form">
<input type="hidden" name="project_id" value="{{ project.id }}">
<div class="form-group">
<label for="board-name">Board Name *</label>
<input type="text" id="board-name" name="name" required maxlength="100">
</div>
<div class="form-group">
<label for="board-description">Description</label>
<textarea id="board-description" name="description" rows="3"></textarea>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="board-is-default" name="is_default">
Set as default board for this project
</label>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-md btn-primary">Create Board</button>
<button type="button" class="btn btn-md btn-secondary" id="cancel-board">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Card Creation/Edit Modal -->
<div id="card-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close">&times;</span>
<h3 id="card-modal-title">Create Card</h3>
<form id="card-form">
<input type="hidden" id="card-id" name="card_id">
<input type="hidden" id="card-column-id" name="column_id">
<div class="form-group">
<label for="card-title">Card Title *</label>
<input type="text" id="card-title" name="title" required maxlength="200">
</div>
<div class="form-group">
<label for="card-description">Description</label>
<textarea id="card-description" name="description" rows="3"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="card-assigned-to">Assigned To</label>
<select id="card-assigned-to" name="assigned_to_id">
<option value="">Unassigned</option>
{% for user in team_members %}
<option value="{{ user.id }}">{{ user.username }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="card-task">Link to Task</label>
<select id="card-task" name="task_id">
<option value="">No task linked</option>
{% for task in tasks %}
<option value="{{ task.id }}">{{ task.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="card-due-date">Due Date</label>
<input type="date" id="card-due-date" name="due_date">
</div>
<div class="form-group">
<label for="card-color">Card Color</label>
<input type="color" id="card-color" name="color" value="#ffffff">
</div>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-md btn-primary">Save Card</button>
<button type="button" class="btn btn-md btn-secondary" id="cancel-card">Cancel</button>
</div>
</form>
</div>
</div>
<!-- Column Management Modal -->
<div id="column-modal" class="modal" style="display: none;">
<div class="modal-content">
<span class="close">&times;</span>
<h3 id="column-modal-title">Manage Columns</h3>
<div class="column-management">
<!-- Add New Column Form -->
<div class="add-column-section">
<h4>Add New Column</h4>
<form id="column-form">
<input type="hidden" id="column-board-id" name="board_id">
<input type="hidden" id="column-id" name="column_id">
<div class="form-row">
<div class="form-group">
<label for="column-name">Column Name *</label>
<input type="text" id="column-name" name="name" required maxlength="100">
</div>
<div class="form-group">
<label for="column-color">Color</label>
<input type="color" id="column-color" name="color" value="#6c757d">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="column-description">Description</label>
<textarea id="column-description" name="description" rows="2"></textarea>
</div>
<div class="form-group">
<label for="column-wip-limit">WIP Limit</label>
<input type="number" id="column-wip-limit" name="wip_limit" min="1" placeholder="Optional">
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-md btn-primary" id="save-column-btn">Add Column</button>
<button type="button" class="btn btn-md btn-secondary" id="cancel-column-edit">Cancel</button>
</div>
</form>
</div>
<!-- Existing Columns List -->
<div class="existing-columns-section">
<h4>Existing Columns</h4>
<div id="columns-list" class="columns-list">
<!-- Columns will be loaded dynamically -->
</div>
</div>
</div>
</div>
</div>
<style>
.kanban-container {
padding: 1rem;
}
.kanban-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #dee2e6;
}
.project-info h2 {
margin: 0 0 0.5rem 0;
}
.project-description {
color: #666;
margin: 0 0 0.5rem 0;
}
.kanban-actions {
display: flex;
gap: 0.75rem;
align-items: center;
flex-wrap: wrap;
}
/* Consistent button sizing */
.btn-md {
padding: 0.5rem 1rem;
font-size: 0.9rem;
font-weight: 500;
line-height: 1.5;
border-radius: 6px;
border: 1px solid transparent;
text-decoration: none;
display: inline-block;
text-align: center;
vertical-align: middle;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-md:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.btn-md:active {
transform: translateY(0);
}
/* Button colors */
.btn-primary {
background-color: #007bff;
border-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
border-color: #004085;
color: white;
}
.btn-success {
background-color: #28a745;
border-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #1e7e34;
border-color: #1c7430;
color: white;
}
.btn-warning {
background-color: #ffc107;
border-color: #ffc107;
color: #212529;
}
.btn-warning:hover {
background-color: #e0a800;
border-color: #d39e00;
color: #212529;
}
.btn-info {
background-color: #17a2b8;
border-color: #17a2b8;
color: white;
}
.btn-info:hover {
background-color: #117a8b;
border-color: #10707f;
color: white;
}
.btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
border-color: #4e555b;
color: white;
}
.board-selection {
margin-bottom: 2rem;
display: flex;
align-items: center;
gap: 1rem;
}
.board-selection label {
font-weight: 500;
margin: 0;
}
.board-selection select {
min-width: 250px;
}
.kanban-board {
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
min-height: 70vh;
overflow-x: auto;
}
.kanban-columns {
display: flex;
gap: 1rem;
min-width: fit-content;
}
.kanban-column {
background: white;
border-radius: 8px;
padding: 1rem;
min-width: 300px;
max-width: 300px;
border: 1px solid #dee2e6;
height: fit-content;
}
.column-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid;
}
.column-title {
font-weight: 600;
margin: 0;
}
.column-count {
background: #e9ecef;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.wip-limit-warning {
background: #fff3cd !important;
color: #856404 !important;
}
.column-cards {
display: flex;
flex-direction: column;
gap: 0.75rem;
min-height: 200px;
}
.kanban-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 0.75rem;
cursor: move;
transition: box-shadow 0.2s ease, transform 0.1s ease;
position: relative;
}
.kanban-card:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
transform: translateY(-1px);
}
.kanban-card.dragging {
opacity: 0.5;
transform: rotate(5deg);
}
.card-title {
font-weight: 600;
margin: 0 0 0.5rem 0;
word-wrap: break-word;
}
.card-description {
color: #666;
font-size: 0.9rem;
margin: 0 0 0.5rem 0;
word-wrap: break-word;
}
.card-meta {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
font-size: 0.8rem;
}
.card-assignee {
background: #e7f3ff;
color: #0066cc;
padding: 0.2rem 0.4rem;
border-radius: 4px;
}
.card-task-link {
background: #f0f9ff;
color: #0284c7;
padding: 0.2rem 0.4rem;
border-radius: 4px;
}
.card-due-date {
background: #fef3c7;
color: #92400e;
padding: 0.2rem 0.4rem;
border-radius: 4px;
}
.card-due-date.overdue {
background: #fee2e2;
color: #dc2626;
}
.card-actions {
position: absolute;
top: 0.5rem;
right: 0.5rem;
opacity: 0;
transition: opacity 0.2s ease;
}
.kanban-card:hover .card-actions {
opacity: 1;
}
.add-card-btn {
width: 100%;
padding: 0.75rem;
border: 2px dashed #dee2e6;
background: transparent;
color: #6c757d;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.9rem;
font-weight: 500;
}
.add-card-btn:hover {
border-color: #007bff;
color: #007bff;
background: #f8f9ff;
}
.no-boards {
text-align: center;
padding: 4rem 2rem;
}
.no-boards-content {
max-width: 400px;
margin: 0 auto;
}
.no-boards h3 {
color: #666;
margin-bottom: 1rem;
}
.no-boards p {
color: #999;
margin-bottom: 2rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.drop-zone {
border: 2px dashed #007bff;
background: #f8f9ff;
border-radius: 6px;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 0.5rem;
}
.drop-zone-text {
color: #007bff;
font-weight: 500;
}
/* Column Management Styles */
.column-management {
max-height: 70vh;
overflow-y: auto;
}
.add-column-section {
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 2rem;
}
.add-column-section h4 {
margin: 0 0 1rem 0;
color: #333;
}
.existing-columns-section h4 {
margin: 0 0 1rem 0;
color: #333;
}
.columns-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.column-item {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
transition: all 0.2s ease;
}
.column-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.column-item.dragging {
opacity: 0.5;
transform: rotate(2deg);
}
.column-info {
flex: 1;
display: flex;
align-items: center;
gap: 1rem;
}
.column-color-indicator {
width: 20px;
height: 20px;
border-radius: 4px;
border: 2px solid #fff;
box-shadow: 0 0 0 1px #dee2e6;
}
.column-details {
flex: 1;
}
.column-item-name {
font-weight: 600;
margin: 0 0 0.25rem 0;
}
.column-meta {
display: flex;
gap: 1rem;
font-size: 0.8rem;
color: #666;
}
.column-wip-limit {
background: #e7f3ff;
color: #0066cc;
padding: 0.2rem 0.4rem;
border-radius: 4px;
}
.column-wip-warning {
background: #fff3cd !important;
color: #856404 !important;
}
.column-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.drag-handle {
cursor: move;
color: #6c757d;
font-size: 1.2rem;
padding: 0.25rem;
}
.drag-handle:hover {
color: #007bff;
}
.form-actions {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
}
.modal-actions {
display: flex;
gap: 0.75rem;
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid #dee2e6;
}
#cancel-column-edit {
display: none;
}
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
.kanban-header {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.kanban-actions {
justify-content: center;
gap: 0.5rem;
}
.kanban-actions .btn-md {
flex: 1;
min-width: 120px;
font-size: 0.8rem;
padding: 0.4rem 0.8rem;
}
.board-selection {
flex-direction: column;
align-items: flex-start;
}
.kanban-columns {
flex-direction: column;
}
.kanban-column {
min-width: auto;
max-width: none;
}
.modal-actions, .form-actions {
flex-direction: column;
}
.modal-actions .btn-md, .form-actions .btn-md {
width: 100%;
}
}
</style>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<script>
let currentBoard = null;
let sortableInstances = [];
document.addEventListener('DOMContentLoaded', function() {
const boardSelect = document.getElementById('board-select');
const kanbanBoard = document.getElementById('kanban-board');
const addCardBtn = document.getElementById('add-card-btn');
// Board selection handler
if (boardSelect) {
boardSelect.addEventListener('change', function() {
const boardId = this.value;
if (boardId) {
loadKanbanBoard(boardId);
addCardBtn.style.display = 'inline-block';
{% if g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator', 'System Administrator'] %}
document.getElementById('manage-columns-btn').style.display = 'inline-block';
{% endif %}
} else {
kanbanBoard.style.display = 'none';
addCardBtn.style.display = 'none';
{% if g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator', 'System Administrator'] %}
document.getElementById('manage-columns-btn').style.display = 'none';
{% endif %}
}
});
// Check URL for board parameter
const urlParams = new URLSearchParams(window.location.search);
const boardFromUrl = urlParams.get('board');
if (boardFromUrl) {
boardSelect.value = boardFromUrl;
loadKanbanBoard(boardFromUrl);
addCardBtn.style.display = 'inline-block';
{% if g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator', 'System Administrator'] %}
document.getElementById('manage-columns-btn').style.display = 'inline-block';
{% endif %}
} else {
// Load default board if exists
const defaultOption = boardSelect.querySelector('option[selected]');
if (defaultOption && defaultOption.value) {
loadKanbanBoard(defaultOption.value);
addCardBtn.style.display = 'inline-block';
{% if g.user.role.value in ['Team Leader', 'Supervisor', 'Administrator', 'System Administrator'] %}
document.getElementById('manage-columns-btn').style.display = 'inline-block';
{% endif %}
}
}
}
// Modal handlers
setupModals();
});
function loadKanbanBoard(boardId) {
fetch(`/api/kanban/boards/${boardId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
currentBoard = data.board;
renderKanbanBoard(data.board);
document.getElementById('kanban-board').style.display = 'block';
} else {
alert('Error loading board: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error loading board');
});
}
function renderKanbanBoard(board) {
const columnsContainer = document.getElementById('kanban-columns');
columnsContainer.innerHTML = '';
// Destroy existing sortable instances
sortableInstances.forEach(instance => instance.destroy());
sortableInstances = [];
board.columns.forEach(column => {
const columnElement = createColumnElement(column);
columnsContainer.appendChild(columnElement);
// Make column sortable for cards
const cardsContainer = columnElement.querySelector('.column-cards');
const sortable = new Sortable(cardsContainer, {
group: 'kanban-cards',
animation: 150,
ghostClass: 'sortable-ghost',
onEnd: function(evt) {
const cardId = evt.item.dataset.cardId;
const newColumnId = evt.to.closest('.kanban-column').dataset.columnId;
const newPosition = evt.newIndex + 1;
moveCard(cardId, newColumnId, newPosition);
}
});
sortableInstances.push(sortable);
});
}
function createColumnElement(column) {
const columnDiv = document.createElement('div');
columnDiv.className = 'kanban-column';
columnDiv.dataset.columnId = column.id;
const wipWarning = column.wip_limit && column.is_over_wip_limit ? 'wip-limit-warning' : '';
columnDiv.innerHTML = `
<div class="column-header" style="border-color: ${column.color};">
<h4 class="column-title" style="color: ${column.color};">${column.name}</h4>
<span class="column-count ${wipWarning}">
${column.card_count}${column.wip_limit ? `/${column.wip_limit}` : ''}
</span>
</div>
<div class="column-cards">
${column.cards.map(card => createCardHTML(card)).join('')}
</div>
<button class="add-card-btn" onclick="openCardModal(${column.id})">
+ Add Card
</button>
`;
return columnDiv;
}
function createCardHTML(card) {
const dueDateClass = card.due_date && new Date(card.due_date) < new Date() ? 'overdue' : '';
const cardStyle = card.color ? `background-color: ${card.color};` : '';
return `
<div class="kanban-card" data-card-id="${card.id}" style="${cardStyle}">
<div class="card-actions">
<button class="btn btn-xs btn-primary" onclick="editCard(${card.id})">Edit</button>
<button class="btn btn-xs btn-danger" onclick="deleteCard(${card.id})">×</button>
</div>
<div class="card-title">${card.title}</div>
${card.description ? `<div class="card-description">${card.description}</div>` : ''}
<div class="card-meta">
${card.assigned_to ? `<span class="card-assignee">${card.assigned_to.username}</span>` : ''}
${card.task_name ? `<span class="card-task-link">${card.task_name}</span>` : ''}
${card.due_date ? `<span class="card-due-date ${dueDateClass}">${formatDate(card.due_date)}</span>` : ''}
</div>
</div>
`;
}
function setupModals() {
const boardModal = document.getElementById('board-modal');
const cardModal = document.getElementById('card-modal');
const boardForm = document.getElementById('board-form');
const cardForm = document.getElementById('card-form');
// Board modal
document.getElementById('create-board-btn')?.addEventListener('click', () => {
boardModal.style.display = 'block';
});
document.getElementById('create-first-board-btn')?.addEventListener('click', () => {
boardModal.style.display = 'block';
});
// Card modal
document.getElementById('add-card-btn')?.addEventListener('click', () => {
if (currentBoard && currentBoard.columns.length > 0) {
openCardModal(currentBoard.columns[0].id);
}
});
// Column management modal
document.getElementById('manage-columns-btn')?.addEventListener('click', () => {
openColumnManagement();
});
// Form submissions
boardForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData);
// Convert checkbox to boolean
data.is_default = document.getElementById('board-is-default').checked;
fetch('/api/kanban/boards', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.success) {
boardModal.style.display = 'none';
location.reload();
} else {
alert('Error: ' + data.message);
}
});
});
cardForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const cardId = formData.get('card_id');
const isEdit = cardId !== '';
const url = isEdit ? `/api/kanban/cards/${cardId}` : '/api/kanban/cards';
const method = isEdit ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(Object.fromEntries(formData))
})
.then(response => response.json())
.then(data => {
if (data.success) {
cardModal.style.display = 'none';
if (isEdit) {
// For edits, just reload the board to be safe
loadKanbanBoard(currentBoard.id);
} else {
// For new cards, update the count immediately
const columnId = formData.get('column_id');
updateColumnCardCountAfterAdd(columnId);
loadKanbanBoard(currentBoard.id); // Still reload for the new card content
}
} else {
alert('Error: ' + data.message);
}
});
});
// Column form submission
const columnForm = document.getElementById('column-form');
columnForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const columnId = formData.get('column_id');
const isEdit = columnId !== '';
const url = isEdit ? `/api/kanban/columns/${columnId}` : '/api/kanban/columns';
const method = isEdit ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(Object.fromEntries(formData))
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadKanbanBoard(currentBoard.id);
if (!isEdit) {
// Reset form for adding more columns
columnForm.reset();
document.getElementById('column-color').value = '#6c757d';
} else {
cancelColumnEdit();
}
loadColumnsList();
} else {
alert('Error: ' + data.message);
}
});
});
// Cancel column edit
document.getElementById('cancel-column-edit').addEventListener('click', () => {
cancelColumnEdit();
});
// Close buttons
document.querySelectorAll('.close').forEach(btn => {
btn.addEventListener('click', function() {
this.closest('.modal').style.display = 'none';
});
});
document.getElementById('cancel-board').addEventListener('click', () => {
boardModal.style.display = 'none';
});
document.getElementById('cancel-card').addEventListener('click', () => {
cardModal.style.display = 'none';
});
// Close modals when clicking outside
const columnModal = document.getElementById('column-modal');
window.addEventListener('click', function(event) {
if (event.target === boardModal) {
boardModal.style.display = 'none';
}
if (event.target === cardModal) {
cardModal.style.display = 'none';
}
if (event.target === columnModal) {
columnModal.style.display = 'none';
}
});
}
function openCardModal(columnId, cardData = null) {
const modal = document.getElementById('card-modal');
const form = document.getElementById('card-form');
const isEdit = cardData !== null;
document.getElementById('card-modal-title').textContent = isEdit ? 'Edit Card' : 'Create Card';
document.getElementById('card-column-id').value = columnId;
if (isEdit) {
document.getElementById('card-id').value = cardData.id;
document.getElementById('card-title').value = cardData.title;
document.getElementById('card-description').value = cardData.description || '';
document.getElementById('card-assigned-to').value = cardData.assigned_to?.id || '';
document.getElementById('card-task').value = cardData.task_id || '';
document.getElementById('card-due-date').value = cardData.due_date || '';
document.getElementById('card-color').value = cardData.color || '#ffffff';
} else {
form.reset();
document.getElementById('card-id').value = '';
document.getElementById('card-column-id').value = columnId;
document.getElementById('card-color').value = '#ffffff';
}
modal.style.display = 'block';
}
function editCard(cardId) {
// Find card data from current board
let cardData = null;
currentBoard.columns.forEach(column => {
const card = column.cards.find(c => c.id === cardId);
if (card) {
cardData = card;
cardData.columnId = column.id;
}
});
if (cardData) {
openCardModal(cardData.columnId, cardData);
}
}
function deleteCard(cardId) {
if (confirm('Are you sure you want to delete this card?')) {
// Find which column the card is in before deleting
let cardColumnId = null;
currentBoard.columns.forEach(column => {
const card = column.cards.find(c => c.id == cardId);
if (card) {
cardColumnId = column.id;
}
});
fetch(`/api/kanban/cards/${cardId}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update the column count immediately
if (cardColumnId) {
updateColumnCardCountAfterDelete(cardColumnId);
}
loadKanbanBoard(currentBoard.id);
} else {
alert('Error: ' + data.message);
}
});
}
}
function moveCard(cardId, columnId, position) {
// Find the card in the current board data
let movedCard = null;
let oldColumnId = null;
currentBoard.columns.forEach(column => {
const cardIndex = column.cards.findIndex(c => c.id == cardId);
if (cardIndex !== -1) {
movedCard = column.cards[cardIndex];
oldColumnId = column.id;
}
});
fetch(`/api/kanban/cards/${cardId}/move`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
column_id: parseInt(columnId),
position: position
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Update card counts in the UI immediately
updateColumnCardCounts(oldColumnId, columnId);
} else {
alert('Error moving card: ' + data.message);
loadKanbanBoard(currentBoard.id); // Reload to reset positions
}
});
}
function updateColumnCardCounts(oldColumnId, newColumnId) {
// Update the old column count (decrease by 1)
if (oldColumnId && oldColumnId != newColumnId) {
const oldColumnElement = document.querySelector(`[data-column-id="${oldColumnId}"]`);
if (oldColumnElement) {
const oldCountElement = oldColumnElement.querySelector('.column-count');
if (oldCountElement) {
const oldCount = parseInt(oldCountElement.textContent.split('/')[0]);
const wipLimit = oldCountElement.textContent.includes('/') ? '/' + oldCountElement.textContent.split('/')[1] : '';
oldCountElement.textContent = (oldCount - 1) + wipLimit;
// Update the current board data
const oldColumn = currentBoard.columns.find(c => c.id == oldColumnId);
if (oldColumn) {
oldColumn.card_count = oldCount - 1;
// Check if still over WIP limit
if (oldColumn.wip_limit && oldColumn.card_count <= oldColumn.wip_limit) {
oldCountElement.classList.remove('wip-limit-warning');
}
}
}
}
}
// Update the new column count (increase by 1)
const newColumnElement = document.querySelector(`[data-column-id="${newColumnId}"]`);
if (newColumnElement) {
const newCountElement = newColumnElement.querySelector('.column-count');
if (newCountElement) {
const newCount = parseInt(newCountElement.textContent.split('/')[0]);
const wipLimit = newCountElement.textContent.includes('/') ? '/' + newCountElement.textContent.split('/')[1] : '';
newCountElement.textContent = (newCount + 1) + wipLimit;
// Update the current board data
const newColumn = currentBoard.columns.find(c => c.id == newColumnId);
if (newColumn) {
newColumn.card_count = newCount + 1;
// Check if now over WIP limit
if (newColumn.wip_limit && newColumn.card_count > newColumn.wip_limit) {
newCountElement.classList.add('wip-limit-warning');
}
}
}
}
}
function updateColumnCardCountAfterAdd(columnId) {
const columnElement = document.querySelector(`[data-column-id="${columnId}"]`);
if (columnElement) {
const countElement = columnElement.querySelector('.column-count');
if (countElement) {
const currentCount = parseInt(countElement.textContent.split('/')[0]);
const wipLimit = countElement.textContent.includes('/') ? '/' + countElement.textContent.split('/')[1] : '';
countElement.textContent = (currentCount + 1) + wipLimit;
// Update the current board data
const column = currentBoard.columns.find(c => c.id == columnId);
if (column) {
column.card_count = currentCount + 1;
// Check if now over WIP limit
if (column.wip_limit && column.card_count > column.wip_limit) {
countElement.classList.add('wip-limit-warning');
}
}
}
}
}
function updateColumnCardCountAfterDelete(columnId) {
const columnElement = document.querySelector(`[data-column-id="${columnId}"]`);
if (columnElement) {
const countElement = columnElement.querySelector('.column-count');
if (countElement) {
const currentCount = parseInt(countElement.textContent.split('/')[0]);
const wipLimit = countElement.textContent.includes('/') ? '/' + countElement.textContent.split('/')[1] : '';
countElement.textContent = (currentCount - 1) + wipLimit;
// Update the current board data
const column = currentBoard.columns.find(c => c.id == columnId);
if (column) {
column.card_count = currentCount - 1;
// Check if no longer over WIP limit
if (column.wip_limit && column.card_count <= column.wip_limit) {
countElement.classList.remove('wip-limit-warning');
}
}
}
}
}
// Column Management Functions
function openColumnManagement() {
if (!currentBoard) {
alert('Please select a board first');
return;
}
const modal = document.getElementById('column-modal');
const form = document.getElementById('column-form');
// Reset form
form.reset();
document.getElementById('column-board-id').value = currentBoard.id;
document.getElementById('column-id').value = '';
document.getElementById('column-color').value = '#6c757d';
document.getElementById('save-column-btn').textContent = 'Add Column';
document.getElementById('cancel-column-edit').style.display = 'none';
// Load existing columns
loadColumnsList();
modal.style.display = 'block';
}
function loadColumnsList() {
const columnsList = document.getElementById('columns-list');
columnsList.innerHTML = '';
if (!currentBoard || !currentBoard.columns) {
return;
}
currentBoard.columns.forEach(column => {
const columnElement = createColumnListItem(column);
columnsList.appendChild(columnElement);
});
// Make columns sortable
if (window.columnSortable) {
window.columnSortable.destroy();
}
window.columnSortable = new Sortable(columnsList, {
animation: 150,
ghostClass: 'sortable-ghost',
handle: '.drag-handle',
onEnd: function(evt) {
const columnId = evt.item.dataset.columnId;
const newPosition = evt.newIndex + 1;
moveColumn(columnId, newPosition);
}
});
}
function createColumnListItem(column) {
const div = document.createElement('div');
div.className = 'column-item';
div.dataset.columnId = column.id;
const wipClass = column.wip_limit && column.is_over_wip_limit ? 'column-wip-warning' : 'column-wip-limit';
div.innerHTML = `
<div class="column-info">
<div class="drag-handle">⋮⋮</div>
<div class="column-color-indicator" style="background-color: ${column.color};"></div>
<div class="column-details">
<div class="column-item-name">${column.name}</div>
<div class="column-meta">
<span>${column.card_count} cards</span>
${column.wip_limit ? `<span class="${wipClass}">WIP: ${column.wip_limit}</span>` : ''}
${column.description ? `<span>${column.description}</span>` : ''}
</div>
</div>
</div>
<div class="column-actions">
<button class="btn btn-xs btn-primary" onclick="editColumn(${column.id})">Edit</button>
<button class="btn btn-xs btn-danger" onclick="deleteColumn(${column.id})">Delete</button>
</div>
`;
return div;
}
function editColumn(columnId) {
const column = currentBoard.columns.find(c => c.id === columnId);
if (!column) return;
const form = document.getElementById('column-form');
document.getElementById('column-id').value = column.id;
document.getElementById('column-name').value = column.name;
document.getElementById('column-description').value = column.description || '';
document.getElementById('column-color').value = column.color;
document.getElementById('column-wip-limit').value = column.wip_limit || '';
document.getElementById('save-column-btn').textContent = 'Update Column';
document.getElementById('cancel-column-edit').style.display = 'inline-block';
}
function cancelColumnEdit() {
const form = document.getElementById('column-form');
form.reset();
document.getElementById('column-id').value = '';
document.getElementById('column-color').value = '#6c757d';
document.getElementById('save-column-btn').textContent = 'Add Column';
document.getElementById('cancel-column-edit').style.display = 'none';
}
function deleteColumn(columnId) {
const column = currentBoard.columns.find(c => c.id === columnId);
if (!column) return;
if (confirm(`Are you sure you want to delete the column "${column.name}"? This action cannot be undone.`)) {
fetch(`/api/kanban/columns/${columnId}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadKanbanBoard(currentBoard.id);
loadColumnsList();
} else {
alert('Error: ' + data.message);
}
});
}
}
function moveColumn(columnId, newPosition) {
fetch(`/api/kanban/columns/${columnId}/move`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
position: newPosition
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
loadKanbanBoard(currentBoard.id);
} else {
alert('Error moving column: ' + data.message);
loadColumnsList(); // Reload to reset positions
}
});
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString();
}
</script>
{% endblock %}