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

@@ -462,7 +462,22 @@
<button class="btn-primary" id="btn-add-task" style="display:none;">+ Uusi tehtävä</button> <button class="btn-primary" id="btn-add-task" style="display:none;">+ Uusi tehtävä</button>
</div> </div>
</div> </div>
<div id="tasks-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem;"></div> <div class="table-card" style="overflow-x:auto;">
<table class="data-table" id="tasks-table" style="display:none;">
<thead>
<tr>
<th>Status</th>
<th>Prioriteetti</th>
<th>Tehtävä</th>
<th>Vastuuhenkilö</th>
<th>Deadline</th>
<th>Tunnit</th>
<th>&#128172;</th>
</tr>
</thead>
<tbody id="tasks-tbody"></tbody>
</table>
</div>
<p id="no-tasks" style="display:none;text-align:center;color:#aaa;padding:3rem 0;">Ei tehtäviä.</p> <p id="no-tasks" style="display:none;text-align:center;color:#aaa;padding:3rem 0;">Ei tehtäviä.</p>
</div> </div>
@@ -585,7 +600,20 @@
<button class="btn-primary" id="btn-add-feature">+ Uusi ehdotus</button> <button class="btn-primary" id="btn-add-feature">+ Uusi ehdotus</button>
</div> </div>
</div> </div>
<div id="features-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem;"></div> <div class="table-card" style="overflow-x:auto;">
<table class="data-table" id="features-table" style="display:none;">
<thead>
<tr>
<th>Status</th>
<th>Ehdotus</th>
<th>Ehdottaja</th>
<th>Päivämäärä</th>
<th>&#128172;</th>
</tr>
</thead>
<tbody id="features-tbody"></tbody>
</table>
</div>
<p id="no-features" style="display:none;text-align:center;color:#aaa;padding:3rem 0;">Ei kehitysehdotuksia.</p> <p id="no-features" style="display:none;text-align:center;color:#aaa;padding:3rem 0;">Ei kehitysehdotuksia.</p>
</div> </div>

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 (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 (statusF) tasks = tasks.filter(t => t.status === statusF);
if (assignF) tasks = tasks.filter(t => t.assigned_to === assignF); if (assignF) tasks = tasks.filter(t => t.assigned_to === assignF);
const grid = document.getElementById('tasks-grid');
const noEl = document.getElementById('no-tasks'); // Lajittelu: deadline lähimmät ensin (null-deadlinet loppuun), sitten prioriteetti
if (!grid) return;
if (!tasks.length) { grid.innerHTML = ''; if (noEl) noEl.style.display = ''; return; }
if (noEl) noEl.style.display = 'none';
const today = new Date().toISOString().slice(0,10); 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 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); 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;"> const rowClass = overdue ? 'todo-row-overdue' : (soon ? 'todo-row-soon' : (t.status === 'valmis' ? 'todo-row-done' : ''));
<div style="display:flex;gap:0.4rem;margin-bottom:0.5rem;flex-wrap:wrap;"> return `<tr class="${rowClass}" onclick="openTaskRead('${t.id}')" style="cursor:pointer;">
<span class="priority-badge priority-${t.priority}">${todoPriorityLabels[t.priority]||t.priority}</span> <td><span class="todo-status status-${t.status}">${todoStatusLabels[t.status]||t.status}</span></td>
<span class="todo-status status-${t.status}">${todoStatusLabels[t.status]||t.status}</span> <td><span class="priority-badge priority-${t.priority}">${todoPriorityLabels[t.priority]||t.priority}</span></td>
</div> <td><strong>${esc(t.title)}</strong></td>
<div style="font-weight:600;font-size:0.95rem;margin-bottom:0.4rem;">${esc(t.title)}</div> <td>${t.assigned_to ? esc(t.assigned_to) : '<span style="color:#ccc;">—</span>'}</td>
<div style="font-size:0.82rem;color:#888;display:flex;gap:0.75rem;flex-wrap:wrap;margin-top:auto;"> <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>
${t.assigned_to ? `<span>&#128100; ${esc(t.assigned_to)}</span>` : ''} <td style="text-align:center;">${t.total_hours > 0 ? t.total_hours + 'h' : '<span style="color:#ccc;">—</span>'}</td>
${t.deadline ? `<span${overdue ? ' style="color:#e74c3c;font-weight:600;"' : ''}>&#128197; ${t.deadline}</span>` : ''} <td style="text-align:center;">${t.comment_count > 0 ? t.comment_count : ''}</td>
${t.total_hours > 0 ? `<span>&#9201; ${t.total_hours}h</span>` : ''} </tr>`;
${t.comment_count > 0 ? `<span>&#128172; ${t.comment_count}</span>` : ''}
</div>
</div>`;
}).join(''); }).join('');
} }
@@ -4093,20 +4110,33 @@ function renderFeaturesList() {
let features = todosData.filter(t => t.type === 'feature_request'); 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 (query) features = features.filter(t => (t.title||'').toLowerCase().includes(query) || (t.description||'').toLowerCase().includes(query));
if (statusF) features = features.filter(t => t.status === statusF); 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'); const noEl = document.getElementById('no-features');
if (!grid) return; if (!tbody) return;
if (!features.length) { grid.innerHTML = ''; if (noEl) noEl.style.display = ''; return; } if (!features.length) { tbody.innerHTML = ''; table.style.display = 'none'; if (noEl) noEl.style.display = ''; return; }
if (noEl) noEl.style.display = 'none'; if (noEl) noEl.style.display = 'none';
grid.innerHTML = features.map(t => `<div class="todo-card" onclick="openFeatureRead('${t.id}')" style="cursor:pointer;"> table.style.display = 'table';
<div style="margin-bottom:0.5rem;"><span class="todo-status status-${t.status}">${todoStatusLabels[t.status]||t.status}</span></div> tbody.innerHTML = features.map(t => {
<div style="font-weight:600;font-size:0.95rem;margin-bottom:0.4rem;">${esc(t.title)}</div> const done = t.status === 'toteutettu' || t.status === 'hylatty';
<div style="font-size:0.82rem;color:#888;display:flex;gap:0.75rem;flex-wrap:wrap;margin-top:auto;"> const rowClass = done ? 'todo-row-done' : '';
<span>&#128100; ${esc(t.created_by)}</span> return `<tr class="${rowClass}" onclick="openFeatureRead('${t.id}')" style="cursor:pointer;">
<span>${(t.luotu||'').slice(0,10)}</span> <td><span class="todo-status status-${t.status}">${todoStatusLabels[t.status]||t.status}</span></td>
${t.comment_count > 0 ? `<span>&#128172; ${t.comment_count}</span>` : ''} <td><strong>${esc(t.title)}</strong></td>
</div> <td>${esc(t.created_by)}</td>
</div>`).join(''); <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() { function showFeatureListView() {

View File

@@ -1129,6 +1129,12 @@ span.empty {
.todo-card:hover { transform:translateY(-2px); box-shadow:0 4px 12px rgba(0,0,0,0.1); border-color:var(--primary-color); } .todo-card:hover { transform:translateY(-2px); box-shadow:0 4px 12px rgba(0,0,0,0.1); border-color:var(--primary-color); }
.todo-card.overdue { border-color:#e74c3c; } .todo-card.overdue { border-color:#e74c3c; }
.todo-card.deadline-soon { border-color:#f39c12; } .todo-card.deadline-soon { border-color:#f39c12; }
.todo-row-overdue { background:#fef2f2 !important; }
.todo-row-overdue:hover { background:#fde8e8 !important; }
.todo-row-soon { background:#fffbeb !important; }
.todo-row-soon:hover { background:#fef3c7 !important; }
.todo-row-done { opacity:0.55; }
.todo-row-done:hover { opacity:0.75; }
.priority-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-size:0.72rem; font-weight:600; } .priority-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-size:0.72rem; font-weight:600; }
.priority-normaali { background:#e8ebf0; color:#555; } .priority-normaali { background:#e8ebf0; color:#555; }
.priority-tarkea { background:#fff3cd; color:#856404; } .priority-tarkea { background:#fff3cd; color:#856404; }