TODO-listaus tauluriveinä korttien sijaan, deadline-lajittelu

Tehtävät ja kehitysehdotukset näytetään nyt taulukkoriveinä
(kuten tukitiketit) kortti-gridin sijaan. Tehtävät lajitellaan
deadlinen mukaan (lähimmät ensin), valmiit loppuun. Myöhästyneet
rivit punaisella ja pian erääntyvät keltaisella taustalla.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 13:26:43 +02:00
parent 4a1dccb6ff
commit 46b40cfc83
3 changed files with 97 additions and 33 deletions

View File

@@ -3943,28 +3943,45 @@ function renderTasksList() {
if (query) tasks = tasks.filter(t => (t.title||'').toLowerCase().includes(query) || (t.description||'').toLowerCase().includes(query) || (t.assigned_to||'').toLowerCase().includes(query));
if (statusF) tasks = tasks.filter(t => t.status === statusF);
if (assignF) tasks = tasks.filter(t => t.assigned_to === assignF);
const grid = document.getElementById('tasks-grid');
const noEl = document.getElementById('no-tasks');
if (!grid) return;
if (!tasks.length) { grid.innerHTML = ''; if (noEl) noEl.style.display = ''; return; }
if (noEl) noEl.style.display = 'none';
// Lajittelu: deadline lähimmät ensin (null-deadlinet loppuun), sitten prioriteetti
const today = new Date().toISOString().slice(0,10);
grid.innerHTML = tasks.map(t => {
const prioOrder = { kiireellinen: 0, tarkea: 1, normaali: 2 };
const statusOrder = { avoin: 0, kaynnissa: 1, odottaa: 2, valmis: 3 };
tasks.sort((a, b) => {
// Valmiit aina loppuun
if ((a.status === 'valmis') !== (b.status === 'valmis')) return a.status === 'valmis' ? 1 : -1;
// Deadline: lähimmät ensin, null loppuun
const da = a.deadline || '9999-99-99';
const db = b.deadline || '9999-99-99';
if (da !== db) return da.localeCompare(db);
// Prioriteetti
const pa = prioOrder[a.priority] ?? 2;
const pb = prioOrder[b.priority] ?? 2;
if (pa !== pb) return pa - pb;
return 0;
});
const tbody = document.getElementById('tasks-tbody');
const table = document.getElementById('tasks-table');
const noEl = document.getElementById('no-tasks');
if (!tbody) return;
if (!tasks.length) { tbody.innerHTML = ''; table.style.display = 'none'; if (noEl) noEl.style.display = ''; return; }
if (noEl) noEl.style.display = 'none';
table.style.display = 'table';
tbody.innerHTML = tasks.map(t => {
const overdue = t.deadline && t.status !== 'valmis' && t.deadline < today;
const soon = t.deadline && t.status !== 'valmis' && !overdue && t.deadline <= new Date(Date.now()+3*86400000).toISOString().slice(0,10);
return `<div class="todo-card${overdue ? ' overdue' : ''}${soon ? ' deadline-soon' : ''}" onclick="openTaskRead('${t.id}')" style="cursor:pointer;">
<div style="display:flex;gap:0.4rem;margin-bottom:0.5rem;flex-wrap:wrap;">
<span class="priority-badge priority-${t.priority}">${todoPriorityLabels[t.priority]||t.priority}</span>
<span class="todo-status status-${t.status}">${todoStatusLabels[t.status]||t.status}</span>
</div>
<div style="font-weight:600;font-size:0.95rem;margin-bottom:0.4rem;">${esc(t.title)}</div>
<div style="font-size:0.82rem;color:#888;display:flex;gap:0.75rem;flex-wrap:wrap;margin-top:auto;">
${t.assigned_to ? `<span>&#128100; ${esc(t.assigned_to)}</span>` : ''}
${t.deadline ? `<span${overdue ? ' style="color:#e74c3c;font-weight:600;"' : ''}>&#128197; ${t.deadline}</span>` : ''}
${t.total_hours > 0 ? `<span>&#9201; ${t.total_hours}h</span>` : ''}
${t.comment_count > 0 ? `<span>&#128172; ${t.comment_count}</span>` : ''}
</div>
</div>`;
const rowClass = overdue ? 'todo-row-overdue' : (soon ? 'todo-row-soon' : (t.status === 'valmis' ? 'todo-row-done' : ''));
return `<tr class="${rowClass}" onclick="openTaskRead('${t.id}')" style="cursor:pointer;">
<td><span class="todo-status status-${t.status}">${todoStatusLabels[t.status]||t.status}</span></td>
<td><span class="priority-badge priority-${t.priority}">${todoPriorityLabels[t.priority]||t.priority}</span></td>
<td><strong>${esc(t.title)}</strong></td>
<td>${t.assigned_to ? esc(t.assigned_to) : '<span style="color:#ccc;">—</span>'}</td>
<td class="nowrap">${t.deadline ? `<span${overdue ? ' style="color:#e74c3c;font-weight:600;"' : (soon ? ' style="color:#f39c12;font-weight:600;"' : '')}>${t.deadline}</span>` : '<span style="color:#ccc;">—</span>'}</td>
<td style="text-align:center;">${t.total_hours > 0 ? t.total_hours + 'h' : '<span style="color:#ccc;">—</span>'}</td>
<td style="text-align:center;">${t.comment_count > 0 ? t.comment_count : ''}</td>
</tr>`;
}).join('');
}
@@ -4093,20 +4110,33 @@ function renderFeaturesList() {
let features = todosData.filter(t => t.type === 'feature_request');
if (query) features = features.filter(t => (t.title||'').toLowerCase().includes(query) || (t.description||'').toLowerCase().includes(query));
if (statusF) features = features.filter(t => t.status === statusF);
const grid = document.getElementById('features-grid');
// Lajittelu: uusimmat ensin, toteutetut/hylätyt loppuun
features.sort((a, b) => {
const doneA = (a.status === 'toteutettu' || a.status === 'hylatty') ? 1 : 0;
const doneB = (b.status === 'toteutettu' || b.status === 'hylatty') ? 1 : 0;
if (doneA !== doneB) return doneA - doneB;
return (b.luotu || '').localeCompare(a.luotu || '');
});
const tbody = document.getElementById('features-tbody');
const table = document.getElementById('features-table');
const noEl = document.getElementById('no-features');
if (!grid) return;
if (!features.length) { grid.innerHTML = ''; if (noEl) noEl.style.display = ''; return; }
if (!tbody) return;
if (!features.length) { tbody.innerHTML = ''; table.style.display = 'none'; if (noEl) noEl.style.display = ''; return; }
if (noEl) noEl.style.display = 'none';
grid.innerHTML = features.map(t => `<div class="todo-card" onclick="openFeatureRead('${t.id}')" style="cursor:pointer;">
<div style="margin-bottom:0.5rem;"><span class="todo-status status-${t.status}">${todoStatusLabels[t.status]||t.status}</span></div>
<div style="font-weight:600;font-size:0.95rem;margin-bottom:0.4rem;">${esc(t.title)}</div>
<div style="font-size:0.82rem;color:#888;display:flex;gap:0.75rem;flex-wrap:wrap;margin-top:auto;">
<span>&#128100; ${esc(t.created_by)}</span>
<span>${(t.luotu||'').slice(0,10)}</span>
${t.comment_count > 0 ? `<span>&#128172; ${t.comment_count}</span>` : ''}
</div>
</div>`).join('');
table.style.display = 'table';
tbody.innerHTML = features.map(t => {
const done = t.status === 'toteutettu' || t.status === 'hylatty';
const rowClass = done ? 'todo-row-done' : '';
return `<tr class="${rowClass}" onclick="openFeatureRead('${t.id}')" style="cursor:pointer;">
<td><span class="todo-status status-${t.status}">${todoStatusLabels[t.status]||t.status}</span></td>
<td><strong>${esc(t.title)}</strong></td>
<td>${esc(t.created_by)}</td>
<td class="nowrap">${(t.luotu||'').slice(0,10)}</td>
<td style="text-align:center;">${t.comment_count > 0 ? t.comment_count : ''}</td>
</tr>`;
}).join('');
}
function showFeatureListView() {