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:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user