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'] ?? '',
'status' => $input['status'] ?? ($type === 'task' ? 'avoin' : 'ehdotettu'),
'priority' => $input['priority'] ?? 'normaali',
'category' => $input['category'] ?? '',
'assigned_to' => $input['assigned_to'] ?? '',
'created_by' => $isNew ? currentUser() : ($input['created_by'] ?? currentUser()),
'deadline' => $input['deadline'] ?? null,

11
db.php
View File

@@ -454,6 +454,7 @@ function initDatabase(): void {
description TEXT,
status VARCHAR(30) NOT NULL DEFAULT 'avoin',
priority VARCHAR(20) DEFAULT 'normaali',
category VARCHAR(30) DEFAULT '',
assigned_to VARCHAR(100) DEFAULT '',
created_by VARCHAR(100) NOT NULL DEFAULT '',
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_body TEXT AFTER auto_reply_enabled",
"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) {
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 {
_dbExecute("
INSERT INTO todos (id, company_id, type, title, description, status, priority, assigned_to, created_by, deadline, luotu, muokattu, muokkaaja)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO todos (id, company_id, type, title, description, status, priority, category, assigned_to, created_by, deadline, luotu, muokattu, muokkaaja)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
title = VALUES(title), description = VALUES(description),
status = VALUES(status), priority = VALUES(priority),
assigned_to = VALUES(assigned_to), deadline = VALUES(deadline),
muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja)
category = VALUES(category), assigned_to = VALUES(assigned_to),
deadline = VALUES(deadline), muokattu = VALUES(muokattu), muokkaaja = VALUES(muokkaaja)
", [
$todo['id'], $companyId,
$todo['type'] ?? 'task',
@@ -1538,6 +1540,7 @@ function dbSaveTodo(string $companyId, array $todo): void {
$todo['description'] ?? '',
$todo['status'] ?? 'avoin',
$todo['priority'] ?? 'normaali',
$todo['category'] ?? '',
$todo['assigned_to'] ?? '',
$todo['created_by'] ?? '',
!empty($todo['deadline']) ? $todo['deadline'] : null,

View File

@@ -456,6 +456,14 @@
<option value="odottaa">Odottaa</option>
<option value="valmis">Valmis</option>
</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;">
<option value="">Kaikki vastuuhenkilöt</option>
</select>
@@ -468,6 +476,7 @@
<tr>
<th>Status</th>
<th>Prioriteetti</th>
<th>Tyyppi</th>
<th>Tehtävä</th>
<th>Vastuuhenkilö</th>
<th>Deadline</th>
@@ -539,7 +548,7 @@
<label for="task-form-title">Otsikko *</label>
<input type="text" id="task-form-title" required placeholder="Tehtävän otsikko">
</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">
<label for="task-form-priority">Prioriteetti</label>
<select id="task-form-priority">
@@ -557,6 +566,17 @@
<option value="valmis">Valmis</option>
</select>
</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">
<label for="task-form-deadline">Deadline</label>
<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 todoPriorityLabels = { normaali:'Normaali', tarkea:'Tärkeä', kiireellinen:'Kiireellinen' };
const todoCategoryLabels = { tekniikka:'Tekniikka', laskutus:'Laskutus', myynti:'Myynti', asennus:'Asennus', muu:'Muu' };
function switchTodoSubTab(target) {
currentTodoSubTab = target;
@@ -3939,10 +3940,12 @@ 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 || '';
const catF = document.getElementById('todo-category-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);
if (catF) tasks = tasks.filter(t => t.category === catF);
// Lajittelu: deadline lähimmät ensin (null-deadlinet loppuun), sitten prioriteetti
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;">
<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>${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>${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>
@@ -4009,6 +4013,7 @@ async function openTaskRead(id) {
<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;">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>`;
// Populoi vastuuhenkilö-dropdown
if (isAdmin) {
@@ -4058,6 +4063,7 @@ async function openTaskEdit(id) {
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-category').value = t?.category || '';
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ä';
@@ -4088,6 +4094,7 @@ document.getElementById('task-form')?.addEventListener('submit', async (e) => {
title: document.getElementById('task-form-title').value.trim(),
description: document.getElementById('task-form-desc').value.trim(),
priority: document.getElementById('task-form-priority').value,
category: document.getElementById('task-form-category').value,
status: document.getElementById('task-form-status').value,
deadline: document.getElementById('task-form-deadline').value || null,
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-status-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-status-filter')?.addEventListener('change', () => renderFeaturesList());
document.getElementById('btn-add-task')?.addEventListener('click', () => openTaskEdit(null));

View File

@@ -1139,6 +1139,12 @@ span.empty {
.priority-normaali { background:#e8ebf0; color:#555; }
.priority-tarkea { background:#fff3cd; color:#856404; }
.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; }
.status-avoin { background:#e3f2fd; color:#1565c0; }
.status-kaynnissa { background:#e8f5e9; color:#2e7d32; }