Tehtäviin Tyyppi-kenttä (tekniikka, laskutus, myynti, asennus, muu)

Uusi category-sarake todosiin. Näkyy listassa badgena, lomakkeessa
dropdownina ja lukunäkymässä. Tyypillä voi myös suodattaa listaa.
Värikoodatut badget kullekin tyypille.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 13:35:17 +02:00
parent cdc5f366ff
commit e3891463e9
5 changed files with 43 additions and 5 deletions

View File

@@ -2254,6 +2254,7 @@ switch ($action) {
'description' => $input['description'] ?? '', 'description' => $input['description'] ?? '',
'status' => $input['status'] ?? ($type === 'task' ? 'avoin' : 'ehdotettu'), 'status' => $input['status'] ?? ($type === 'task' ? 'avoin' : 'ehdotettu'),
'priority' => $input['priority'] ?? 'normaali', 'priority' => $input['priority'] ?? 'normaali',
'category' => $input['category'] ?? '',
'assigned_to' => $input['assigned_to'] ?? '', 'assigned_to' => $input['assigned_to'] ?? '',
'created_by' => $isNew ? currentUser() : ($input['created_by'] ?? currentUser()), 'created_by' => $isNew ? currentUser() : ($input['created_by'] ?? currentUser()),
'deadline' => $input['deadline'] ?? null, 'deadline' => $input['deadline'] ?? null,

11
db.php
View File

@@ -454,6 +454,7 @@ function initDatabase(): void {
description TEXT, description TEXT,
status VARCHAR(30) NOT NULL DEFAULT 'avoin', status VARCHAR(30) NOT NULL DEFAULT 'avoin',
priority VARCHAR(20) DEFAULT 'normaali', priority VARCHAR(20) DEFAULT 'normaali',
category VARCHAR(30) DEFAULT '',
assigned_to VARCHAR(100) DEFAULT '', assigned_to VARCHAR(100) DEFAULT '',
created_by VARCHAR(100) NOT NULL DEFAULT '', created_by VARCHAR(100) NOT NULL DEFAULT '',
deadline DATE DEFAULT NULL, deadline DATE DEFAULT NULL,
@@ -518,6 +519,7 @@ function initDatabase(): void {
"ALTER TABLE mailboxes ADD COLUMN auto_reply_enabled BOOLEAN DEFAULT FALSE AFTER aktiivinen", "ALTER TABLE mailboxes ADD COLUMN auto_reply_enabled BOOLEAN DEFAULT FALSE AFTER aktiivinen",
"ALTER TABLE mailboxes ADD COLUMN auto_reply_body TEXT AFTER auto_reply_enabled", "ALTER TABLE mailboxes ADD COLUMN auto_reply_body TEXT AFTER auto_reply_enabled",
"ALTER TABLE companies ADD COLUMN allowed_ips TEXT DEFAULT '' AFTER enabled_modules", "ALTER TABLE companies ADD COLUMN allowed_ips TEXT DEFAULT '' AFTER enabled_modules",
"ALTER TABLE todos ADD COLUMN category VARCHAR(30) DEFAULT '' AFTER priority",
]; ];
foreach ($alters as $sql) { foreach ($alters as $sql) {
try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ } try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ }
@@ -1524,13 +1526,13 @@ function dbLoadTodo(string $todoId): ?array {
function dbSaveTodo(string $companyId, array $todo): void { function dbSaveTodo(string $companyId, array $todo): void {
_dbExecute(" _dbExecute("
INSERT INTO todos (id, company_id, type, title, description, status, priority, assigned_to, created_by, deadline, luotu, muokattu, muokkaaja) INSERT INTO todos (id, company_id, type, title, description, status, priority, category, assigned_to, created_by, deadline, luotu, muokattu, muokkaaja)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
title = VALUES(title), description = VALUES(description), title = VALUES(title), description = VALUES(description),
status = VALUES(status), priority = VALUES(priority), status = VALUES(status), priority = VALUES(priority),
assigned_to = VALUES(assigned_to), deadline = VALUES(deadline), category = VALUES(category), assigned_to = VALUES(assigned_to),
muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja) deadline = VALUES(deadline), muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja)
", [ ", [
$todo['id'], $companyId, $todo['id'], $companyId,
$todo['type'] ?? 'task', $todo['type'] ?? 'task',
@@ -1538,6 +1540,7 @@ function dbSaveTodo(string $companyId, array $todo): void {
$todo['description'] ?? '', $todo['description'] ?? '',
$todo['status'] ?? 'avoin', $todo['status'] ?? 'avoin',
$todo['priority'] ?? 'normaali', $todo['priority'] ?? 'normaali',
$todo['category'] ?? '',
$todo['assigned_to'] ?? '', $todo['assigned_to'] ?? '',
$todo['created_by'] ?? '', $todo['created_by'] ?? '',
!empty($todo['deadline']) ? $todo['deadline'] : null, !empty($todo['deadline']) ? $todo['deadline'] : null,

View File

@@ -456,6 +456,14 @@
<option value="odottaa">Odottaa</option> <option value="odottaa">Odottaa</option>
<option value="valmis">Valmis</option> <option value="valmis">Valmis</option>
</select> </select>
<select id="todo-category-filter" style="padding:0.4rem 0.6rem;border-radius:6px;border:1px solid #ddd;font-size:0.85rem;">
<option value="">Kaikki tyypit</option>
<option value="tekniikka">Tekniikka</option>
<option value="laskutus">Laskutus</option>
<option value="myynti">Myynti</option>
<option value="asennus">Asennus</option>
<option value="muu">Muu</option>
</select>
<select id="todo-assigned-filter" style="padding:0.4rem 0.6rem;border-radius:6px;border:1px solid #ddd;font-size:0.85rem;"> <select id="todo-assigned-filter" style="padding:0.4rem 0.6rem;border-radius:6px;border:1px solid #ddd;font-size:0.85rem;">
<option value="">Kaikki vastuuhenkilöt</option> <option value="">Kaikki vastuuhenkilöt</option>
</select> </select>
@@ -468,6 +476,7 @@
<tr> <tr>
<th>Status</th> <th>Status</th>
<th>Prioriteetti</th> <th>Prioriteetti</th>
<th>Tyyppi</th>
<th>Tehtävä</th> <th>Tehtävä</th>
<th>Vastuuhenkilö</th> <th>Vastuuhenkilö</th>
<th>Deadline</th> <th>Deadline</th>
@@ -539,7 +548,7 @@
<label for="task-form-title">Otsikko *</label> <label for="task-form-title">Otsikko *</label>
<input type="text" id="task-form-title" required placeholder="Tehtävän otsikko"> <input type="text" id="task-form-title" required placeholder="Tehtävän otsikko">
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:1rem;margin-bottom:1rem;"> <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:1rem;margin-bottom:1rem;">
<div class="form-group"> <div class="form-group">
<label for="task-form-priority">Prioriteetti</label> <label for="task-form-priority">Prioriteetti</label>
<select id="task-form-priority"> <select id="task-form-priority">
@@ -557,6 +566,17 @@
<option value="valmis">Valmis</option> <option value="valmis">Valmis</option>
</select> </select>
</div> </div>
<div class="form-group">
<label for="task-form-category">Tyyppi</label>
<select id="task-form-category">
<option value="">— Ei valittu —</option>
<option value="tekniikka">Tekniikka</option>
<option value="laskutus">Laskutus</option>
<option value="myynti">Myynti</option>
<option value="asennus">Asennus</option>
<option value="muu">Muu</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label for="task-form-deadline">Deadline</label> <label for="task-form-deadline">Deadline</label>
<input type="date" id="task-form-deadline"> <input type="date" id="task-form-deadline">

View File

@@ -3898,6 +3898,7 @@ let currentTodoSubTab = 'tasks';
const todoStatusLabels = { avoin:'Avoin', kaynnissa:'Käynnissä', odottaa:'Odottaa', valmis:'Valmis', ehdotettu:'Ehdotettu', harkinnassa:'Harkinnassa', toteutettu:'Toteutettu', hylatty:'Hylätty' }; 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' }; const todoPriorityLabels = { normaali:'Normaali', tarkea:'Tärkeä', kiireellinen:'Kiireellinen' };
const todoCategoryLabels = { tekniikka:'Tekniikka', laskutus:'Laskutus', myynti:'Myynti', asennus:'Asennus', muu:'Muu' };
function switchTodoSubTab(target) { function switchTodoSubTab(target) {
currentTodoSubTab = target; currentTodoSubTab = target;
@@ -3939,10 +3940,12 @@ function renderTasksList() {
const query = (document.getElementById('todo-search-input')?.value || '').toLowerCase().trim(); const query = (document.getElementById('todo-search-input')?.value || '').toLowerCase().trim();
const statusF = document.getElementById('todo-status-filter')?.value || ''; const statusF = document.getElementById('todo-status-filter')?.value || '';
const assignF = document.getElementById('todo-assigned-filter')?.value || ''; const assignF = document.getElementById('todo-assigned-filter')?.value || '';
const catF = document.getElementById('todo-category-filter')?.value || '';
let tasks = todosData.filter(t => t.type === 'task'); 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 (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);
if (catF) tasks = tasks.filter(t => t.category === catF);
// Lajittelu: deadline lähimmät ensin (null-deadlinet loppuun), sitten prioriteetti // Lajittelu: deadline lähimmät ensin (null-deadlinet loppuun), sitten prioriteetti
const today = new Date().toISOString().slice(0,10); const today = new Date().toISOString().slice(0,10);
@@ -3976,6 +3979,7 @@ function renderTasksList() {
return `<tr class="${rowClass}" onclick="openTaskRead('${t.id}')" style="cursor:pointer;"> 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="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><span class="priority-badge priority-${t.priority}">${todoPriorityLabels[t.priority]||t.priority}</span></td>
<td>${t.category ? `<span class="todo-category cat-${t.category}">${todoCategoryLabels[t.category]||t.category}</span>` : '<span style="color:#ccc;">—</span>'}</td>
<td><strong>${esc(t.title)}</strong></td> <td><strong>${esc(t.title)}</strong></td>
<td>${t.assigned_to ? esc(t.assigned_to) : '<span style="color:#ccc;">—</span>'}</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 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>
@@ -4009,6 +4013,7 @@ async function openTaskRead(id) {
<option value="">— Ei —</option> <option value="">— Ei —</option>
</select>` : esc(t.assigned_to || '—')}</div> </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;">Prioriteetti</strong><br>${todoPriorityLabels[t.priority]||t.priority}</div>
<div><strong style="font-size:0.78rem;color:#888;">Tyyppi</strong><br>${t.category ? (todoCategoryLabels[t.category]||t.category) : '—'}</div>
<div><strong style="font-size:0.78rem;color:#888;">Deadline</strong><br>${t.deadline || '—'}</div>`; <div><strong style="font-size:0.78rem;color:#888;">Deadline</strong><br>${t.deadline || '—'}</div>`;
// Populoi vastuuhenkilö-dropdown // Populoi vastuuhenkilö-dropdown
if (isAdmin) { if (isAdmin) {
@@ -4058,6 +4063,7 @@ async function openTaskEdit(id) {
document.getElementById('task-form-title').value = t?.title || ''; document.getElementById('task-form-title').value = t?.title || '';
document.getElementById('task-form-priority').value = t?.priority || 'normaali'; document.getElementById('task-form-priority').value = t?.priority || 'normaali';
document.getElementById('task-form-status').value = t?.status || 'avoin'; document.getElementById('task-form-status').value = t?.status || 'avoin';
document.getElementById('task-form-category').value = t?.category || '';
document.getElementById('task-form-deadline').value = t?.deadline || ''; document.getElementById('task-form-deadline').value = t?.deadline || '';
document.getElementById('task-form-desc').value = t?.description || ''; document.getElementById('task-form-desc').value = t?.description || '';
document.getElementById('task-edit-title').textContent = t ? 'Muokkaa tehtävää' : 'Uusi tehtävä'; document.getElementById('task-edit-title').textContent = t ? 'Muokkaa tehtävää' : 'Uusi tehtävä';
@@ -4088,6 +4094,7 @@ document.getElementById('task-form')?.addEventListener('submit', async (e) => {
title: document.getElementById('task-form-title').value.trim(), title: document.getElementById('task-form-title').value.trim(),
description: document.getElementById('task-form-desc').value.trim(), description: document.getElementById('task-form-desc').value.trim(),
priority: document.getElementById('task-form-priority').value, priority: document.getElementById('task-form-priority').value,
category: document.getElementById('task-form-category').value,
status: document.getElementById('task-form-status').value, status: document.getElementById('task-form-status').value,
deadline: document.getElementById('task-form-deadline').value || null, deadline: document.getElementById('task-form-deadline').value || null,
assigned_to: document.getElementById('task-form-assigned').value, assigned_to: document.getElementById('task-form-assigned').value,
@@ -4275,6 +4282,7 @@ async function deleteTimeEntry(entryId, todoId) {
document.getElementById('todo-search-input')?.addEventListener('input', () => renderTasksList()); document.getElementById('todo-search-input')?.addEventListener('input', () => renderTasksList());
document.getElementById('todo-status-filter')?.addEventListener('change', () => renderTasksList()); document.getElementById('todo-status-filter')?.addEventListener('change', () => renderTasksList());
document.getElementById('todo-assigned-filter')?.addEventListener('change', () => renderTasksList()); document.getElementById('todo-assigned-filter')?.addEventListener('change', () => renderTasksList());
document.getElementById('todo-category-filter')?.addEventListener('change', () => renderTasksList());
document.getElementById('feature-search-input')?.addEventListener('input', () => renderFeaturesList()); document.getElementById('feature-search-input')?.addEventListener('input', () => renderFeaturesList());
document.getElementById('feature-status-filter')?.addEventListener('change', () => renderFeaturesList()); document.getElementById('feature-status-filter')?.addEventListener('change', () => renderFeaturesList());
document.getElementById('btn-add-task')?.addEventListener('click', () => openTaskEdit(null)); document.getElementById('btn-add-task')?.addEventListener('click', () => openTaskEdit(null));

View File

@@ -1139,6 +1139,12 @@ span.empty {
.priority-normaali { background:#e8ebf0; color:#555; } .priority-normaali { background:#e8ebf0; color:#555; }
.priority-tarkea { background:#fff3cd; color:#856404; } .priority-tarkea { background:#fff3cd; color:#856404; }
.priority-kiireellinen { background:#fde8e8; color:#dc2626; } .priority-kiireellinen { background:#fde8e8; color:#dc2626; }
.todo-category { display:inline-block; padding:2px 8px; border-radius:10px; font-size:0.72rem; font-weight:600; }
.cat-tekniikka { background:#e0f2fe; color:#0369a1; }
.cat-laskutus { background:#fef3c7; color:#92400e; }
.cat-myynti { background:#d1fae5; color:#065f46; }
.cat-asennus { background:#ede9fe; color:#5b21b6; }
.cat-muu { background:#f3f4f6; color:#6b7280; }
.todo-status { display:inline-block; padding:2px 8px; border-radius:10px; font-size:0.72rem; font-weight:600; } .todo-status { display:inline-block; padding:2px 8px; border-radius:10px; font-size:0.72rem; font-weight:600; }
.status-avoin { background:#e3f2fd; color:#1565c0; } .status-avoin { background:#e3f2fd; color:#1565c0; }
.status-kaynnissa { background:#e8f5e9; color:#2e7d32; } .status-kaynnissa { background:#e8f5e9; color:#2e7d32; }