feat(tasks): add quick-status button to kanban cards (#24)

Adds a small button on each kanban card that cycles the task status
(open → in_progress → done → open) without requiring drag-and-drop.
Useful for touch devices and kiosk browsers (e.g. Fully Kiosk Browser)
where drag-and-drop is unavailable. All four locales updated.
This commit is contained in:
Ulas
2026-04-05 16:16:46 +02:00
parent 31a9538518
commit 19a7161307
8 changed files with 95 additions and 9 deletions
+5
View File
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.11.7] - 2026-04-05
### Added
- Kanban view: quick-status button on each card to advance status without drag-and-drop (open → in progress → done → open) - useful for touch devices and kiosk browsers (#24)
## [0.11.6] - 2026-04-05
### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "oikos",
"version": "0.11.6",
"version": "0.11.7",
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
"main": "server/index.js",
"type": "module",
+3
View File
@@ -142,6 +142,9 @@
"kanbanOpen": "Offen",
"kanbanInProgress": "In Bearbeitung",
"kanbanDone": "Erledigt",
"kanbanMoveToInProgress": "In Bearbeitung setzen",
"kanbanMoveToDone": "Als erledigt markieren",
"kanbanMoveToOpen": "Erneut öffnen",
"recurring": "Wiederkehrend",
"listView": "Listenansicht",
"kanbanView": "Kanban-Ansicht"
+3
View File
@@ -142,6 +142,9 @@
"kanbanOpen": "Open",
"kanbanInProgress": "In Progress",
"kanbanDone": "Done",
"kanbanMoveToInProgress": "Set to in progress",
"kanbanMoveToDone": "Mark as done",
"kanbanMoveToOpen": "Reopen",
"recurring": "Recurring",
"listView": "List view",
"kanbanView": "Kanban view"
+3
View File
@@ -142,6 +142,9 @@
"kanbanOpen": "Da fare",
"kanbanInProgress": "In corso",
"kanbanDone": "Completato",
"kanbanMoveToInProgress": "Imposta in corso",
"kanbanMoveToDone": "Segna come completato",
"kanbanMoveToOpen": "Riapri",
"recurring": "Ricorrente",
"listView": "Vista elenco",
"kanbanView": "Vista Kanban"
+3
View File
@@ -142,6 +142,9 @@
"kanbanOpen": "Öppna",
"kanbanInProgress": "Pågår",
"kanbanDone": "Slutfört",
"kanbanMoveToInProgress": "Sätt till pågår",
"kanbanMoveToDone": "Markera som klar",
"kanbanMoveToOpen": "Öppna igen",
"recurring": "Återkommande",
"listView": "Listvy",
"kanbanView": "Kanban-vy"
+46 -7
View File
@@ -506,8 +506,21 @@ const KANBAN_COLS = () => [
{ status: 'done', label: t('tasks.kanbanDone'), colorVar: '--color-success' },
];
function kanbanNextStatus(status) {
if (status === 'open') return 'in_progress';
if (status === 'in_progress') return 'done';
return 'open';
}
function renderKanbanCard(task) {
const due = formatDueDate(task.due_date);
const due = formatDueDate(task.due_date);
const next = kanbanNextStatus(task.status);
const icon = next === 'done' ? 'check' : next === 'in_progress' ? 'circle-play' : 'rotate-ccw';
const nextLabel = next === 'done'
? t('tasks.kanbanMoveToDone')
: next === 'in_progress'
? t('tasks.kanbanMoveToInProgress')
: t('tasks.kanbanMoveToOpen');
return `
<div class="kanban-card ${task.status === 'done' ? 'kanban-card--done' : ''}"
data-task-id="${task.id}" draggable="true">
@@ -516,13 +529,17 @@ function renderKanbanCard(task) {
${renderPriorityBadge(task.priority)}
${due ? `<span class="due-date ${due.cls}"><i data-lucide="clock" style="width:10px;height:10px" aria-hidden="true"></i> ${due.label}</span>` : ''}
</div>
${task.assigned_color ? `
<div class="kanban-card__footer">
<div class="kanban-card__footer">
${task.assigned_color ? `
<div class="task-avatar" style="background-color:${task.assigned_color};width:22px;height:22px;font-size:9px"
title="${task.assigned_name ?? ''}">
title="${esc(task.assigned_name ?? '')}">
${initials(task.assigned_name ?? '')}
</div>
</div>` : ''}
</div>` : '<span></span>'}
<button class="kanban-card__status-btn" type="button"
data-next-status="${next}" title="${nextLabel}" aria-label="${nextLabel}">
<i data-lucide="${icon}" aria-hidden="true"></i>
</button>
</div>
</div>`;
}
@@ -625,8 +642,30 @@ function wireKanbanDrag(container) {
}
});
// Klick auf Kanban-Card öffnet Edit-Modal
// Klick auf Status-Button: Status ohne Modal wechseln
board.addEventListener('click', async (e) => {
const statusBtn = e.target.closest('[data-next-status]');
if (statusBtn) {
e.stopPropagation();
const card = statusBtn.closest('.kanban-card[data-task-id]');
if (!card) return;
const taskId = card.dataset.taskId;
const newStatus = statusBtn.dataset.nextStatus;
const task = state.tasks.find((t) => String(t.id) === String(taskId));
if (!task) return;
task.status = newStatus;
renderKanban(container);
try {
await api.patch(`/tasks/${taskId}/status`, { status: newStatus });
await loadTasks(container);
} catch (err) {
window.oikos.showToast(err.message, 'danger');
await loadTasks(container);
}
return;
}
// Klick auf Kanban-Card öffnet Edit-Modal
if (e.target.closest('[draggable]')) {
const card = e.target.closest('.kanban-card[data-task-id]');
if (!card) return;
+31 -1
View File
@@ -581,10 +581,40 @@
.kanban-card__footer {
display: flex;
justify-content: flex-end;
align-items: center;
justify-content: space-between;
margin-top: var(--space-2);
}
.kanban-card__status-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
transition: background-color var(--transition-fast), color var(--transition-fast),
border-color var(--transition-fast);
flex-shrink: 0;
}
.kanban-card__status-btn:hover {
background-color: var(--color-accent-light);
border-color: var(--color-accent);
color: var(--color-accent);
}
.kanban-card__status-btn svg {
width: 12px;
height: 12px;
pointer-events: none;
}
.kanban-drop-placeholder {
height: 60px;
border: 2px dashed var(--color-accent);