Uusi TODO-moduuli: Tehtävät + Kehitysehdotukset + Ajanseuranta
Talon sisäinen tehtävienhallinta kahdella alatabilla: Tehtävät (admin luo): - Prioriteetti (normaali/tärkeä/kiireellinen), status, deadline - Vastuuhenkilö-osoitus, inline-muokkaus lukunäkymässä - Aikakirjaukset: pvm, tunnit, kuvaus - kaikki voivat kirjata - Myöhästyneet = punainen reunus, lähestyvät = keltainen - Kommentointi kaikille käyttäjille Kehitysehdotukset (kaikki voivat luoda): - Status: ehdotettu → harkinnassa → toteutettu/hylätty (admin muuttaa) - Kommentointi kaikille - Ehdottaja voi muokata omia Tietokanta: 3 taulua (todos, todo_comments, todo_time_entries) API: 10 endpointtia oikeustarkistuksineen Frontend: Sub-tab navigointi, kortti-grid, 3-näkymämalli per alatabi Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
382
script.js
382
script.js
@@ -195,7 +195,7 @@ async function showDashboard() {
|
||||
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
const [mainHash, subHash] = hash.split('/');
|
||||
const validTabs = ['customers', 'leads', 'tekniikka', 'ohjeet', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
|
||||
const validTabs = ['customers', 'leads', 'tekniikka', 'ohjeet', 'todo', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
|
||||
const startTab = validTabs.includes(mainHash) ? mainHash : 'customers';
|
||||
switchToTab(startTab, subHash);
|
||||
}
|
||||
@@ -256,6 +256,7 @@ function switchToTab(target, subTab) {
|
||||
if (target === 'archive') loadArchive();
|
||||
if (target === 'changelog') loadChangelog();
|
||||
if (target === 'ohjeet') loadGuides();
|
||||
if (target === 'todo') { loadTodos(); if (subTab) switchTodoSubTab(subTab); }
|
||||
if (target === 'support') { loadTickets(); showTicketListView(); if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh(); }
|
||||
if (target === 'users') loadUsers();
|
||||
if (target === 'settings') loadSettings();
|
||||
@@ -3889,9 +3890,386 @@ async function deleteGuideCategory(id, name) {
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
// ==================== TEHTÄVÄT (TODO) ====================
|
||||
|
||||
let todosData = [];
|
||||
let currentTodoId = null;
|
||||
let currentTodoSubTab = 'tasks';
|
||||
|
||||
const todoStatusLabels = { avoin:'Avoin', kaynnissa:'Käynnissä', odottaa:'Odottaa', valmis:'Valmis', ehdotettu:'Ehdotettu', harkinnassa:'Harkinnassa', toteutettu:'Toteutettu', hylatty:'Hylätty' };
|
||||
const todoPriorityLabels = { normaali:'Normaali', tarkea:'Tärkeä', kiireellinen:'Kiireellinen' };
|
||||
|
||||
function switchTodoSubTab(target) {
|
||||
currentTodoSubTab = target;
|
||||
document.querySelectorAll('[data-todotab]').forEach(b => {
|
||||
b.classList.toggle('active', b.dataset.todotab === target);
|
||||
b.style.borderBottomColor = b.dataset.todotab === target ? 'var(--primary-color)' : 'transparent';
|
||||
b.style.color = b.dataset.todotab === target ? 'var(--primary-color)' : '#888';
|
||||
});
|
||||
document.getElementById('todo-subtab-tasks').style.display = target === 'tasks' ? '' : 'none';
|
||||
document.getElementById('todo-subtab-features').style.display = target === 'features' ? '' : 'none';
|
||||
// Palauta listanäkymään kun vaihdetaan tabia
|
||||
if (target === 'tasks') showTaskListView();
|
||||
if (target === 'features') showFeatureListView();
|
||||
window.location.hash = 'todo/' + target;
|
||||
}
|
||||
|
||||
async function loadTodos() {
|
||||
try {
|
||||
todosData = await apiCall('todos');
|
||||
renderTasksList();
|
||||
renderFeaturesList();
|
||||
populateTodoAssignedFilter();
|
||||
const isAdmin = currentUser?.role === 'admin' || currentUser?.role === 'superadmin';
|
||||
const btnTask = document.getElementById('btn-add-task');
|
||||
if (btnTask) btnTask.style.display = isAdmin ? '' : 'none';
|
||||
} catch (e) { console.error('loadTodos:', e); }
|
||||
}
|
||||
|
||||
function populateTodoAssignedFilter() {
|
||||
const sel = document.getElementById('todo-assigned-filter');
|
||||
if (!sel) return;
|
||||
const users = [...new Set(todosData.filter(t => t.assigned_to).map(t => t.assigned_to))].sort();
|
||||
sel.innerHTML = '<option value="">Kaikki vastuuhenkilöt</option>' + users.map(u => `<option value="${esc(u)}">${esc(u)}</option>`).join('');
|
||||
}
|
||||
|
||||
// ---- Tehtävät ----
|
||||
|
||||
function renderTasksList() {
|
||||
const query = (document.getElementById('todo-search-input')?.value || '').toLowerCase().trim();
|
||||
const statusF = document.getElementById('todo-status-filter')?.value || '';
|
||||
const assignF = document.getElementById('todo-assigned-filter')?.value || '';
|
||||
let tasks = todosData.filter(t => t.type === 'task');
|
||||
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';
|
||||
const today = new Date().toISOString().slice(0,10);
|
||||
grid.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>👤 ${esc(t.assigned_to)}</span>` : ''}
|
||||
${t.deadline ? `<span${overdue ? ' style="color:#e74c3c;font-weight:600;"' : ''}>📅 ${t.deadline}</span>` : ''}
|
||||
${t.total_hours > 0 ? `<span>⏱ ${t.total_hours}h</span>` : ''}
|
||||
${t.comment_count > 0 ? `<span>💬 ${t.comment_count}</span>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showTaskListView() {
|
||||
document.getElementById('tasks-list-view').style.display = '';
|
||||
document.getElementById('task-read-view').style.display = 'none';
|
||||
document.getElementById('task-edit-view').style.display = 'none';
|
||||
}
|
||||
|
||||
async function openTaskRead(id) {
|
||||
currentTodoId = id;
|
||||
try {
|
||||
const t = await apiCall('todo_detail&id=' + id);
|
||||
const isAdmin = currentUser?.role === 'admin' || currentUser?.role === 'superadmin';
|
||||
document.getElementById('task-read-title').textContent = t.title;
|
||||
document.getElementById('task-read-meta').innerHTML = `Luoja: ${esc(t.created_by)} | Luotu: ${(t.luotu||'').slice(0,10)} ${t.muokattu ? ' | Muokattu: ' + t.muokattu.slice(0,10) : ''}`;
|
||||
document.getElementById('task-read-badges').innerHTML = `
|
||||
<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>`;
|
||||
document.getElementById('task-read-fields').innerHTML = `
|
||||
<div><strong style="font-size:0.78rem;color:#888;">Status</strong><br>${isAdmin ? `<select onchange="updateTaskField('${t.id}','status',this.value)" style="font-size:0.88rem;padding:0.25rem 0.5rem;border-radius:6px;border:1px solid #ddd;">
|
||||
<option value="avoin" ${t.status==='avoin'?'selected':''}>Avoin</option><option value="kaynnissa" ${t.status==='kaynnissa'?'selected':''}>Käynnissä</option><option value="odottaa" ${t.status==='odottaa'?'selected':''}>Odottaa</option><option value="valmis" ${t.status==='valmis'?'selected':''}>Valmis</option>
|
||||
</select>` : (todoStatusLabels[t.status]||t.status)}</div>
|
||||
<div><strong style="font-size:0.78rem;color:#888;">Vastuuhenkilö</strong><br>${isAdmin ? `<select onchange="updateTaskField('${t.id}','assigned',this.value)" style="font-size:0.88rem;padding:0.25rem 0.5rem;border-radius:6px;border:1px solid #ddd;" id="task-read-assigned-sel">
|
||||
<option value="">— Ei —</option>
|
||||
</select>` : esc(t.assigned_to || '—')}</div>
|
||||
<div><strong style="font-size:0.78rem;color:#888;">Prioriteetti</strong><br>${todoPriorityLabels[t.priority]||t.priority}</div>
|
||||
<div><strong style="font-size:0.78rem;color:#888;">Deadline</strong><br>${t.deadline || '—'}</div>`;
|
||||
// Populoi vastuuhenkilö-dropdown
|
||||
if (isAdmin) {
|
||||
try {
|
||||
const users = await apiCall('users');
|
||||
const sel = document.getElementById('task-read-assigned-sel');
|
||||
if (sel) { users.forEach(u => { const o = document.createElement('option'); o.value = u.username; o.textContent = u.nimi || u.username; if (u.username === t.assigned_to) o.selected = true; sel.appendChild(o); }); }
|
||||
} catch(e) {}
|
||||
}
|
||||
document.getElementById('task-read-description').textContent = t.description || '(Ei kuvausta)';
|
||||
// Aikakirjaukset
|
||||
const entries = t.time_entries || [];
|
||||
document.getElementById('task-time-total').textContent = `(yhteensä: ${t.total_hours || 0}h)`;
|
||||
document.getElementById('task-time-tbody').innerHTML = entries.length ? entries.map(e => `<tr>
|
||||
<td>${e.work_date}</td><td>${esc(e.user)}</td><td>${e.hours}h</td><td>${esc(e.description||'')}</td>
|
||||
<td>${(e.user === currentUser?.username || isAdmin) ? `<button onclick="deleteTimeEntry('${e.id}','${t.id}')" style="background:none;border:none;cursor:pointer;color:#ccc;font-size:1rem;" title="Poista">🗑</button>` : ''}</td>
|
||||
</tr>`).join('') : '<tr><td colspan="5" style="color:#aaa;text-align:center;">Ei kirjauksia</td></tr>';
|
||||
// Kommentit
|
||||
renderTodoComments(t.comments || [], 'task');
|
||||
document.getElementById('task-comment-count').textContent = `(${(t.comments||[]).length})`;
|
||||
// Actionit
|
||||
document.getElementById('task-read-actions').innerHTML = isAdmin ? `<button class="btn-secondary" onclick="openTaskEdit('${t.id}')">✎ Muokkaa</button><button class="btn-secondary" onclick="deleteTodo('${t.id}')" style="color:#e74c3c;">🗑 Poista</button>` : '';
|
||||
// Aikakirjaus-lomake valmistelu
|
||||
document.getElementById('time-form-date').value = new Date().toISOString().slice(0,10);
|
||||
document.getElementById('time-form-hours').value = '';
|
||||
document.getElementById('time-form-desc').value = '';
|
||||
document.getElementById('task-time-form').style.display = 'none';
|
||||
document.getElementById('tasks-list-view').style.display = 'none';
|
||||
document.getElementById('task-edit-view').style.display = 'none';
|
||||
document.getElementById('task-read-view').style.display = '';
|
||||
} catch (e) { alert('Virhe: ' + e.message); }
|
||||
}
|
||||
|
||||
async function updateTaskField(id, field, value) {
|
||||
try {
|
||||
if (field === 'status') await apiCall('todo_status', 'POST', { id, status: value });
|
||||
if (field === 'assigned') await apiCall('todo_assign', 'POST', { id, assigned_to: value });
|
||||
await loadTodos();
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
async function openTaskEdit(id) {
|
||||
const t = id ? todosData.find(x => x.id === id) : null;
|
||||
currentTodoId = t?.id || null;
|
||||
document.getElementById('task-form-id').value = t?.id || '';
|
||||
document.getElementById('task-form-type').value = 'task';
|
||||
document.getElementById('task-form-title').value = t?.title || '';
|
||||
document.getElementById('task-form-priority').value = t?.priority || 'normaali';
|
||||
document.getElementById('task-form-status').value = t?.status || 'avoin';
|
||||
document.getElementById('task-form-deadline').value = t?.deadline || '';
|
||||
document.getElementById('task-form-desc').value = t?.description || '';
|
||||
document.getElementById('task-edit-title').textContent = t ? 'Muokkaa tehtävää' : 'Uusi tehtävä';
|
||||
// Populoi vastuuhenkilö-dropdown
|
||||
const asel = document.getElementById('task-form-assigned');
|
||||
asel.innerHTML = '<option value="">— Ei vastuuhenkilöä —</option>';
|
||||
try {
|
||||
const users = await apiCall('users');
|
||||
users.forEach(u => {
|
||||
const o = document.createElement('option');
|
||||
o.value = u.username; o.textContent = u.nimi || u.username;
|
||||
if (t && u.username === t.assigned_to) o.selected = true;
|
||||
asel.appendChild(o);
|
||||
});
|
||||
} catch(e) {}
|
||||
document.getElementById('tasks-list-view').style.display = 'none';
|
||||
document.getElementById('task-read-view').style.display = 'none';
|
||||
document.getElementById('task-edit-view').style.display = '';
|
||||
}
|
||||
|
||||
document.getElementById('task-form')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('task-form-id').value;
|
||||
const existing = id ? todosData.find(t => t.id === id) : null;
|
||||
const body = {
|
||||
id: id || undefined,
|
||||
type: 'task',
|
||||
title: document.getElementById('task-form-title').value.trim(),
|
||||
description: document.getElementById('task-form-desc').value.trim(),
|
||||
priority: document.getElementById('task-form-priority').value,
|
||||
status: document.getElementById('task-form-status').value,
|
||||
deadline: document.getElementById('task-form-deadline').value || null,
|
||||
assigned_to: document.getElementById('task-form-assigned').value,
|
||||
created_by: existing?.created_by,
|
||||
luotu: existing?.luotu,
|
||||
};
|
||||
if (!body.title) return;
|
||||
try {
|
||||
const saved = await apiCall('todo_save', 'POST', body);
|
||||
await loadTodos();
|
||||
openTaskRead(saved.id);
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
|
||||
// ---- Kehitysehdotukset ----
|
||||
|
||||
function renderFeaturesList() {
|
||||
const query = (document.getElementById('feature-search-input')?.value || '').toLowerCase().trim();
|
||||
const statusF = document.getElementById('feature-status-filter')?.value || '';
|
||||
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');
|
||||
const noEl = document.getElementById('no-features');
|
||||
if (!grid) return;
|
||||
if (!features.length) { grid.innerHTML = ''; 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>👤 ${esc(t.created_by)}</span>
|
||||
<span>${(t.luotu||'').slice(0,10)}</span>
|
||||
${t.comment_count > 0 ? `<span>💬 ${t.comment_count}</span>` : ''}
|
||||
</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function showFeatureListView() {
|
||||
document.getElementById('features-list-view').style.display = '';
|
||||
document.getElementById('feature-read-view').style.display = 'none';
|
||||
document.getElementById('feature-edit-view').style.display = 'none';
|
||||
}
|
||||
|
||||
async function openFeatureRead(id) {
|
||||
currentTodoId = id;
|
||||
try {
|
||||
const t = await apiCall('todo_detail&id=' + id);
|
||||
const isAdmin = currentUser?.role === 'admin' || currentUser?.role === 'superadmin';
|
||||
const isOwner = t.created_by === currentUser?.username;
|
||||
document.getElementById('feature-read-title').textContent = t.title;
|
||||
document.getElementById('feature-read-meta').innerHTML = `Ehdottaja: ${esc(t.created_by)} | ${(t.luotu||'').slice(0,10)}`;
|
||||
document.getElementById('feature-read-badges').innerHTML = isAdmin
|
||||
? `<select onchange="updateTaskField('${t.id}','status',this.value)" style="font-size:0.88rem;padding:0.3rem 0.6rem;border-radius:6px;border:1px solid #ddd;">
|
||||
<option value="ehdotettu" ${t.status==='ehdotettu'?'selected':''}>Ehdotettu</option><option value="harkinnassa" ${t.status==='harkinnassa'?'selected':''}>Harkinnassa</option><option value="toteutettu" ${t.status==='toteutettu'?'selected':''}>Toteutettu</option><option value="hylatty" ${t.status==='hylatty'?'selected':''}>Hylätty</option>
|
||||
</select>`
|
||||
: `<span class="todo-status status-${t.status}">${todoStatusLabels[t.status]||t.status}</span>`;
|
||||
document.getElementById('feature-read-description').textContent = t.description || '(Ei kuvausta)';
|
||||
renderTodoComments(t.comments || [], 'feature');
|
||||
document.getElementById('feature-comment-count').textContent = `(${(t.comments||[]).length})`;
|
||||
document.getElementById('feature-read-actions').innerHTML = (isAdmin || isOwner)
|
||||
? `<button class="btn-secondary" onclick="openFeatureEdit('${t.id}')">✎ Muokkaa</button>${isAdmin ? `<button class="btn-secondary" onclick="deleteTodo('${t.id}')" style="color:#e74c3c;">🗑 Poista</button>` : ''}`
|
||||
: '';
|
||||
document.getElementById('features-list-view').style.display = 'none';
|
||||
document.getElementById('feature-edit-view').style.display = 'none';
|
||||
document.getElementById('feature-read-view').style.display = '';
|
||||
} catch (e) { alert('Virhe: ' + e.message); }
|
||||
}
|
||||
|
||||
async function openFeatureEdit(id) {
|
||||
const t = id ? todosData.find(x => x.id === id) : null;
|
||||
currentTodoId = t?.id || null;
|
||||
document.getElementById('feature-form-id').value = t?.id || '';
|
||||
document.getElementById('feature-form-type').value = 'feature_request';
|
||||
document.getElementById('feature-form-title').value = t?.title || '';
|
||||
document.getElementById('feature-form-desc').value = t?.description || '';
|
||||
document.getElementById('feature-edit-title').textContent = t ? 'Muokkaa ehdotusta' : 'Uusi kehitysehdotus';
|
||||
document.getElementById('features-list-view').style.display = 'none';
|
||||
document.getElementById('feature-read-view').style.display = 'none';
|
||||
document.getElementById('feature-edit-view').style.display = '';
|
||||
}
|
||||
|
||||
document.getElementById('feature-form')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('feature-form-id').value;
|
||||
const existing = id ? todosData.find(t => t.id === id) : null;
|
||||
const body = {
|
||||
id: id || undefined,
|
||||
type: 'feature_request',
|
||||
title: document.getElementById('feature-form-title').value.trim(),
|
||||
description: document.getElementById('feature-form-desc').value.trim(),
|
||||
status: existing?.status || 'ehdotettu',
|
||||
priority: 'normaali',
|
||||
created_by: existing?.created_by,
|
||||
luotu: existing?.luotu,
|
||||
};
|
||||
if (!body.title) return;
|
||||
try {
|
||||
const saved = await apiCall('todo_save', 'POST', body);
|
||||
await loadTodos();
|
||||
openFeatureRead(saved.id);
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
|
||||
// ---- Yhteiset funktiot ----
|
||||
|
||||
function renderTodoComments(comments, prefix) {
|
||||
const list = document.getElementById(prefix + '-comments-list');
|
||||
if (!list) return;
|
||||
const isAdmin = currentUser?.role === 'admin' || currentUser?.role === 'superadmin';
|
||||
list.innerHTML = comments.length ? comments.map(c => `<div class="todo-comment">
|
||||
<div class="todo-comment-meta">${esc(c.author)} · ${(c.luotu||'').replace('T',' ').slice(0,16)}</div>
|
||||
<div style="white-space:pre-wrap;">${esc(c.body)}</div>
|
||||
${(c.author === currentUser?.username || isAdmin) ? `<button onclick="deleteTodoComment('${c.id}')" style="background:none;border:none;color:#ccc;cursor:pointer;font-size:0.78rem;margin-top:0.25rem;">Poista</button>` : ''}
|
||||
</div>`).join('') : '<p style="color:#aaa;font-size:0.88rem;">Ei kommentteja vielä.</p>';
|
||||
}
|
||||
|
||||
async function sendTodoComment(prefix) {
|
||||
const input = document.getElementById(prefix + '-comment-input');
|
||||
const body = input?.value.trim();
|
||||
if (!body || !currentTodoId) return;
|
||||
try {
|
||||
await apiCall('todo_comment', 'POST', { todo_id: currentTodoId, body });
|
||||
input.value = '';
|
||||
if (prefix === 'task') await openTaskRead(currentTodoId);
|
||||
else await openFeatureRead(currentTodoId);
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
async function deleteTodoComment(commentId) {
|
||||
if (!confirm('Poistetaanko kommentti?')) return;
|
||||
try {
|
||||
await apiCall('todo_comment_delete', 'POST', { id: commentId });
|
||||
if (currentTodoSubTab === 'tasks') await openTaskRead(currentTodoId);
|
||||
else await openFeatureRead(currentTodoId);
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
async function deleteTodo(id) {
|
||||
if (!confirm('Poistetaanko pysyvästi?')) return;
|
||||
try {
|
||||
await apiCall('todo_delete', 'POST', { id });
|
||||
await loadTodos();
|
||||
if (currentTodoSubTab === 'tasks') showTaskListView();
|
||||
else showFeatureListView();
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
async function addTimeEntry() {
|
||||
if (!currentTodoId) return;
|
||||
const hours = parseFloat(document.getElementById('time-form-hours').value);
|
||||
const desc = document.getElementById('time-form-desc').value.trim();
|
||||
const date = document.getElementById('time-form-date').value;
|
||||
if (!hours || hours <= 0) { alert('Syötä tunnit'); return; }
|
||||
try {
|
||||
await apiCall('todo_time_add', 'POST', { todo_id: currentTodoId, hours, description: desc, work_date: date });
|
||||
await loadTodos();
|
||||
await openTaskRead(currentTodoId);
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
async function deleteTimeEntry(entryId, todoId) {
|
||||
if (!confirm('Poistetaanko aikakirjaus?')) return;
|
||||
try {
|
||||
await apiCall('todo_time_delete', 'POST', { id: entryId });
|
||||
await loadTodos();
|
||||
await openTaskRead(todoId);
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('todo-search-input')?.addEventListener('input', () => renderTasksList());
|
||||
document.getElementById('todo-status-filter')?.addEventListener('change', () => renderTasksList());
|
||||
document.getElementById('todo-assigned-filter')?.addEventListener('change', () => renderTasksList());
|
||||
document.getElementById('feature-search-input')?.addEventListener('input', () => renderFeaturesList());
|
||||
document.getElementById('feature-status-filter')?.addEventListener('change', () => renderFeaturesList());
|
||||
document.getElementById('btn-add-task')?.addEventListener('click', () => openTaskEdit(null));
|
||||
document.getElementById('btn-add-feature')?.addEventListener('click', () => openFeatureEdit(null));
|
||||
document.getElementById('btn-task-back')?.addEventListener('click', () => { showTaskListView(); currentTodoId = null; });
|
||||
document.getElementById('btn-feature-back')?.addEventListener('click', () => { showFeatureListView(); currentTodoId = null; });
|
||||
document.getElementById('btn-task-edit-cancel')?.addEventListener('click', () => showTaskListView());
|
||||
document.getElementById('task-form-cancel')?.addEventListener('click', () => showTaskListView());
|
||||
document.getElementById('btn-feature-edit-cancel')?.addEventListener('click', () => showFeatureListView());
|
||||
document.getElementById('feature-form-cancel')?.addEventListener('click', () => showFeatureListView());
|
||||
document.getElementById('btn-task-comment-send')?.addEventListener('click', () => sendTodoComment('task'));
|
||||
document.getElementById('btn-feature-comment-send')?.addEventListener('click', () => sendTodoComment('feature'));
|
||||
document.getElementById('btn-add-time')?.addEventListener('click', () => {
|
||||
document.getElementById('task-time-form').style.display = 'flex';
|
||||
document.getElementById('btn-add-time').style.display = 'none';
|
||||
});
|
||||
document.getElementById('btn-time-cancel')?.addEventListener('click', () => {
|
||||
document.getElementById('task-time-form').style.display = 'none';
|
||||
document.getElementById('btn-add-time').style.display = '';
|
||||
});
|
||||
document.getElementById('btn-time-save')?.addEventListener('click', () => addTimeEntry());
|
||||
|
||||
// ==================== MODUULIT ====================
|
||||
|
||||
const ALL_MODULES = ['customers', 'support', 'leads', 'tekniikka', 'ohjeet', 'archive', 'changelog', 'settings'];
|
||||
const ALL_MODULES = ['customers', 'support', 'leads', 'tekniikka', 'ohjeet', 'todo', 'archive', 'changelog', 'settings'];
|
||||
const DEFAULT_MODULES = ['customers', 'support', 'archive', 'changelog', 'settings'];
|
||||
|
||||
function applyModules(modules) {
|
||||
|
||||
Reference in New Issue
Block a user