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:
2026-03-11 13:14:53 +02:00
parent ec86263c5c
commit 4a1dccb6ff
5 changed files with 958 additions and 2 deletions

200
api.php
View File

@@ -2208,6 +2208,206 @@ switch ($action) {
readfile($path);
exit;
// ---------- TEHTÄVÄT (TODOS) ----------
case 'todos':
requireAuth();
$companyId = requireCompany();
echo json_encode(dbLoadTodos($companyId));
break;
case 'todo_detail':
requireAuth();
$companyId = requireCompany();
$id = $_GET['id'] ?? '';
$todo = dbLoadTodo($id);
if (!$todo || $todo['company_id'] !== $companyId) {
http_response_code(404);
echo json_encode(['error' => 'Tehtävää ei löydy']);
break;
}
unset($todo['company_id']);
echo json_encode($todo);
break;
case 'todo_save':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$type = $input['type'] ?? 'task';
$isNew = empty($input['id']);
// Task: vain admin. Feature request: kaikki voivat luoda, mutta muokata vain omia (tai admin)
if ($type === 'task') {
requireAdmin();
} elseif (!$isNew) {
$existing = dbLoadTodo($input['id']);
if ($existing && $existing['created_by'] !== currentUser() && !isAdmin()) {
http_response_code(403);
echo json_encode(['error' => 'Voit muokata vain omia ehdotuksiasi']);
break;
}
}
$todo = [
'id' => $input['id'] ?? generateId(),
'type' => $type,
'title' => trim($input['title'] ?? ''),
'description' => $input['description'] ?? '',
'status' => $input['status'] ?? ($type === 'task' ? 'avoin' : 'ehdotettu'),
'priority' => $input['priority'] ?? 'normaali',
'assigned_to' => $input['assigned_to'] ?? '',
'created_by' => $isNew ? currentUser() : ($input['created_by'] ?? currentUser()),
'deadline' => $input['deadline'] ?? null,
'luotu' => $isNew ? date('Y-m-d H:i:s') : ($input['luotu'] ?? date('Y-m-d H:i:s')),
'muokkaaja' => currentUser(),
];
if (empty($todo['title'])) {
http_response_code(400);
echo json_encode(['error' => 'Otsikko on pakollinen']);
break;
}
dbSaveTodo($companyId, $todo);
dbAddLog($companyId, currentUser(), $isNew ? 'todo_create' : 'todo_update', $todo['id'], $todo['title'], '');
echo json_encode($todo);
break;
case 'todo_delete':
requireAuth();
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$todo = dbLoadTodo($id);
if ($todo) {
dbDeleteTodo($id);
dbAddLog($companyId, currentUser(), 'todo_delete', $id, $todo['title'] ?? '', '');
}
echo json_encode(['success' => true]);
break;
case 'todo_status':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$status = $input['status'] ?? '';
$todo = dbLoadTodo($id);
if (!$todo || $todo['company_id'] !== $companyId) {
http_response_code(404); break;
}
// Feature request status: vain admin
if ($todo['type'] === 'feature_request' && !isAdmin()) {
http_response_code(403);
echo json_encode(['error' => 'Vain admin voi muuttaa ehdotuksen statusta']);
break;
}
// Task status: vain admin
if ($todo['type'] === 'task' && !isAdmin()) {
http_response_code(403); break;
}
$todo['status'] = $status;
$todo['muokkaaja'] = currentUser();
dbSaveTodo($companyId, $todo);
echo json_encode(['success' => true]);
break;
case 'todo_assign':
requireAuth();
requireAdmin();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
$todo = dbLoadTodo($id);
if (!$todo || $todo['company_id'] !== $companyId) {
http_response_code(404); break;
}
$todo['assigned_to'] = $input['assigned_to'] ?? '';
$todo['muokkaaja'] = currentUser();
dbSaveTodo($companyId, $todo);
echo json_encode(['success' => true]);
break;
case 'todo_comment':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$todoId = $input['todo_id'] ?? '';
$body = trim($input['body'] ?? '');
if (empty($body)) {
http_response_code(400);
echo json_encode(['error' => 'Kommentti ei voi olla tyhjä']);
break;
}
$comment = [
'id' => generateId(),
'author' => currentUser(),
'body' => $body,
'luotu' => date('Y-m-d H:i:s'),
];
dbAddTodoComment($todoId, $comment);
echo json_encode($comment);
break;
case 'todo_comment_delete':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$commentId = $input['id'] ?? '';
// Tarkista onko oma kommentti tai admin
$rows = _dbFetchAll("SELECT author FROM todo_comments WHERE id = ?", [$commentId]);
if (!empty($rows) && ($rows[0]['author'] === currentUser() || isAdmin())) {
dbDeleteTodoComment($commentId);
echo json_encode(['success' => true]);
} else {
http_response_code(403);
echo json_encode(['error' => 'Ei oikeutta']);
}
break;
case 'todo_time_add':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$todoId = $input['todo_id'] ?? '';
$hours = floatval($input['hours'] ?? 0);
if ($hours <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Tunnit pitää olla > 0']);
break;
}
$entry = [
'id' => generateId(),
'user' => currentUser(),
'hours' => $hours,
'description' => trim($input['description'] ?? ''),
'work_date' => $input['work_date'] ?? date('Y-m-d'),
'luotu' => date('Y-m-d H:i:s'),
];
dbAddTodoTimeEntry($todoId, $entry);
echo json_encode($entry);
break;
case 'todo_time_delete':
requireAuth();
$companyId = requireCompany();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$entryId = $input['id'] ?? '';
$rows = _dbFetchAll("SELECT user FROM todo_time_entries WHERE id = ?", [$entryId]);
if (!empty($rows) && ($rows[0]['user'] === currentUser() || isAdmin())) {
dbDeleteTodoTimeEntry($entryId);
echo json_encode(['success' => true]);
} else {
http_response_code(403);
echo json_encode(['error' => 'Ei oikeutta']);
}
break;
// ---------- ARCHIVE ----------
case 'archived_customers':
requireAuth();