feat(tasks): add optional "none" priority level for tasks without urgency
New tasks default to "none" priority instead of "medium". Tasks with no priority hide the badge in list and dashboard views, reducing visual noise for routine items. Includes DB migration v4 and i18n keys (de, en, it). Closes #15
This commit is contained in:
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.9.0] - 2026-04-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Optional task priority: new "None" level allows tasks without urgency, reducing visual noise for routine tasks (#15)
|
||||||
|
- "None" is now the default priority for new tasks
|
||||||
|
- Tasks with no priority hide the priority badge entirely in list and dashboard views
|
||||||
|
- DB migration v4 extends priority CHECK constraint to include 'none'
|
||||||
|
- i18n keys for "None" priority in de, en, it locales
|
||||||
|
|
||||||
## [0.8.2] - 2026-04-04
|
## [0.8.2] - 2026-04-04
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oikos",
|
"name": "oikos",
|
||||||
"version": "0.8.2",
|
"version": "0.9.0",
|
||||||
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"priorityHigh": "Hoch",
|
"priorityHigh": "Hoch",
|
||||||
"priorityMedium": "Mittel",
|
"priorityMedium": "Mittel",
|
||||||
"priorityLow": "Niedrig",
|
"priorityLow": "Niedrig",
|
||||||
|
"priorityNone": "Keine",
|
||||||
"statusOpen": "Offen",
|
"statusOpen": "Offen",
|
||||||
"statusInProgress": "In Bearbeitung",
|
"statusInProgress": "In Bearbeitung",
|
||||||
"statusDone": "Erledigt",
|
"statusDone": "Erledigt",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"priorityHigh": "High",
|
"priorityHigh": "High",
|
||||||
"priorityMedium": "Medium",
|
"priorityMedium": "Medium",
|
||||||
"priorityLow": "Low",
|
"priorityLow": "Low",
|
||||||
|
"priorityNone": "None",
|
||||||
"statusOpen": "Open",
|
"statusOpen": "Open",
|
||||||
"statusInProgress": "In Progress",
|
"statusInProgress": "In Progress",
|
||||||
"statusDone": "Done",
|
"statusDone": "Done",
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
"priorityHigh": "Alta",
|
"priorityHigh": "Alta",
|
||||||
"priorityMedium": "Media",
|
"priorityMedium": "Media",
|
||||||
"priorityLow": "Bassa",
|
"priorityLow": "Bassa",
|
||||||
|
"priorityNone": "Nessuna",
|
||||||
"statusOpen": "Aperto",
|
"statusOpen": "Aperto",
|
||||||
"statusInProgress": "In corso",
|
"statusInProgress": "In corso",
|
||||||
"statusDone": "Completato",
|
"statusDone": "Completato",
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ function renderUrgentTasks(tasks) {
|
|||||||
const due = formatDueDate(t.due_date);
|
const due = formatDueDate(t.due_date);
|
||||||
return `
|
return `
|
||||||
<div class="task-item" data-route="/tasks" role="button" tabindex="0">
|
<div class="task-item" data-route="/tasks" role="button" tabindex="0">
|
||||||
<div class="task-item__priority task-item__priority--${t.priority}" aria-hidden="true"></div>
|
${t.priority !== 'none' ? `<div class="task-item__priority task-item__priority--${t.priority}" aria-hidden="true"></div>` : ''}
|
||||||
<span class="sr-only">${PRIORITY_LABELS()[t.priority] ?? t.priority}</span>
|
<span class="sr-only">${PRIORITY_LABELS()[t.priority] ?? t.priority}</span>
|
||||||
<div class="task-item__content">
|
<div class="task-item__content">
|
||||||
<div class="task-item__title">${esc(t.title)}</div>
|
<div class="task-item__title">${esc(t.title)}</div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const PRIORITIES = () => [
|
|||||||
{ value: 'high', label: t('tasks.priorityHigh'), color: 'var(--color-priority-high)' },
|
{ value: 'high', label: t('tasks.priorityHigh'), color: 'var(--color-priority-high)' },
|
||||||
{ value: 'medium', label: t('tasks.priorityMedium'), color: 'var(--color-priority-medium)' },
|
{ value: 'medium', label: t('tasks.priorityMedium'), color: 'var(--color-priority-medium)' },
|
||||||
{ value: 'low', label: t('tasks.priorityLow'), color: 'var(--color-priority-low)' },
|
{ value: 'low', label: t('tasks.priorityLow'), color: 'var(--color-priority-low)' },
|
||||||
|
{ value: 'none', label: t('tasks.priorityNone'), color: 'var(--color-priority-none)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const STATUSES = () => [
|
const STATUSES = () => [
|
||||||
@@ -110,6 +111,7 @@ function groupBy(tasks, mode) {
|
|||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function renderPriorityBadge(priority) {
|
function renderPriorityBadge(priority) {
|
||||||
|
if (priority === 'none') return '';
|
||||||
return `<span class="priority-badge priority-badge--${priority}">
|
return `<span class="priority-badge priority-badge--${priority}">
|
||||||
<span class="priority-dot priority-dot--${priority}"></span>
|
<span class="priority-dot priority-dot--${priority}"></span>
|
||||||
${PRIORITY_LABELS()[priority] ?? priority}
|
${PRIORITY_LABELS()[priority] ?? priority}
|
||||||
@@ -254,7 +256,7 @@ function renderModalContent({ task = null, users = [] } = {}) {
|
|||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
const priorityOptions = PRIORITIES().map((p) =>
|
const priorityOptions = PRIORITIES().map((p) =>
|
||||||
`<option value="${p.value}" ${(task?.priority ?? 'medium') === p.value ? 'selected' : ''}>${p.label}</option>`
|
`<option value="${p.value}" ${(task?.priority ?? 'none') === p.value ? 'selected' : ''}>${p.label}</option>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
|||||||
@@ -114,12 +114,14 @@
|
|||||||
/* --------------------------------------------------------
|
/* --------------------------------------------------------
|
||||||
* 6. Farben - Prioritäten
|
* 6. Farben - Prioritäten
|
||||||
* -------------------------------------------------------- */
|
* -------------------------------------------------------- */
|
||||||
|
--color-priority-none: var(--neutral-400);
|
||||||
--color-priority-low: var(--neutral-500);
|
--color-priority-low: var(--neutral-500);
|
||||||
--color-priority-medium: #B45309;
|
--color-priority-medium: #B45309;
|
||||||
--color-priority-high: #D4511E;
|
--color-priority-high: #D4511E;
|
||||||
--color-priority-urgent: #DC2626;
|
--color-priority-urgent: #DC2626;
|
||||||
|
|
||||||
/* Hintergrundfarben für Priority-Badges */
|
/* Hintergrundfarben für Priority-Badges */
|
||||||
|
--color-priority-none-bg: rgba(142, 141, 137, 0.08);
|
||||||
--color-priority-low-bg: rgba(142, 141, 137, 0.12);
|
--color-priority-low-bg: rgba(142, 141, 137, 0.12);
|
||||||
--color-priority-medium-bg: rgba(180, 83, 9, 0.12);
|
--color-priority-medium-bg: rgba(180, 83, 9, 0.12);
|
||||||
--color-priority-high-bg: rgba(212, 81, 30, 0.12);
|
--color-priority-high-bg: rgba(212, 81, 30, 0.12);
|
||||||
@@ -328,6 +330,7 @@
|
|||||||
--meal-snack-light: #3D2010;
|
--meal-snack-light: #3D2010;
|
||||||
|
|
||||||
/* Priority-Badge Hintergründe */
|
/* Priority-Badge Hintergründe */
|
||||||
|
--color-priority-none-bg: rgba(142, 141, 137, 0.12);
|
||||||
--color-priority-low-bg: rgba(142, 141, 137, 0.18);
|
--color-priority-low-bg: rgba(142, 141, 137, 0.18);
|
||||||
--color-priority-medium-bg: rgba(230, 147, 10, 0.18);
|
--color-priority-medium-bg: rgba(230, 147, 10, 0.18);
|
||||||
--color-priority-high-bg: rgba(212, 81, 30, 0.18);
|
--color-priority-high-bg: rgba(212, 81, 30, 0.18);
|
||||||
@@ -408,6 +411,7 @@
|
|||||||
--meal-dinner-light: #1A2D4D;
|
--meal-dinner-light: #1A2D4D;
|
||||||
--meal-snack-light: #3D2010;
|
--meal-snack-light: #3D2010;
|
||||||
|
|
||||||
|
--color-priority-none-bg: rgba(142, 141, 137, 0.12);
|
||||||
--color-priority-low-bg: rgba(142, 141, 137, 0.18);
|
--color-priority-low-bg: rgba(142, 141, 137, 0.18);
|
||||||
--color-priority-medium-bg: rgba(230, 147, 10, 0.18);
|
--color-priority-medium-bg: rgba(230, 147, 10, 0.18);
|
||||||
--color-priority-high-bg: rgba(212, 81, 30, 0.18);
|
--color-priority-high-bg: rgba(212, 81, 30, 0.18);
|
||||||
|
|||||||
+3
-3
@@ -12,9 +12,9 @@
|
|||||||
* API: Immer Netzwerk (kein Caching von Nutzerdaten)
|
* API: Immer Netzwerk (kein Caching von Nutzerdaten)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SHELL_CACHE = 'oikos-shell-v23';
|
const SHELL_CACHE = 'oikos-shell-v24';
|
||||||
const PAGES_CACHE = 'oikos-pages-v23';
|
const PAGES_CACHE = 'oikos-pages-v24';
|
||||||
const ASSETS_CACHE = 'oikos-assets-v23';
|
const ASSETS_CACHE = 'oikos-assets-v24';
|
||||||
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
|
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, ASSETS_CACHE];
|
||||||
|
|
||||||
// App-Shell: sofort benötigt für ersten Render
|
// App-Shell: sofort benötigt für ersten Render
|
||||||
|
|||||||
+37
-2
@@ -88,8 +88,8 @@ const MIGRATIONS = [
|
|||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
category TEXT NOT NULL DEFAULT 'Sonstiges',
|
category TEXT NOT NULL DEFAULT 'Sonstiges',
|
||||||
priority TEXT NOT NULL DEFAULT 'medium'
|
priority TEXT NOT NULL DEFAULT 'none'
|
||||||
CHECK(priority IN ('low', 'medium', 'high', 'urgent')),
|
CHECK(priority IN ('none', 'low', 'medium', 'high', 'urgent')),
|
||||||
status TEXT NOT NULL DEFAULT 'open'
|
status TEXT NOT NULL DEFAULT 'open'
|
||||||
CHECK(status IN ('open', 'in_progress', 'done')),
|
CHECK(status IN ('open', 'in_progress', 'done')),
|
||||||
due_date TEXT,
|
due_date TEXT,
|
||||||
@@ -296,6 +296,41 @@ const MIGRATIONS = [
|
|||||||
CREATE INDEX IF NOT EXISTS idx_budget_parent ON budget_entries(recurrence_parent_id);
|
CREATE INDEX IF NOT EXISTS idx_budget_parent ON budget_entries(recurrence_parent_id);
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
version: 4,
|
||||||
|
description: 'Priorität "none" erlauben und als Default setzen',
|
||||||
|
up: `
|
||||||
|
-- SQLite erlaubt kein ALTER CHECK, daher Tabelle neu erstellen
|
||||||
|
CREATE TABLE tasks_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
category TEXT NOT NULL DEFAULT 'Sonstiges',
|
||||||
|
priority TEXT NOT NULL DEFAULT 'none'
|
||||||
|
CHECK(priority IN ('none', 'low', 'medium', 'high', 'urgent')),
|
||||||
|
status TEXT NOT NULL DEFAULT 'open'
|
||||||
|
CHECK(status IN ('open', 'in_progress', 'done')),
|
||||||
|
due_date TEXT,
|
||||||
|
due_time TEXT,
|
||||||
|
assigned_to INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
is_recurring INTEGER NOT NULL DEFAULT 0,
|
||||||
|
recurrence_rule TEXT,
|
||||||
|
parent_task_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO tasks_new SELECT * FROM tasks;
|
||||||
|
DROP TABLE tasks;
|
||||||
|
ALTER TABLE tasks_new RENAME TO tasks;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_to);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_task_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(due_date);
|
||||||
|
`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const router = express.Router();
|
|||||||
// Konstanten
|
// Konstanten
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
const VALID_PRIORITIES = ['low', 'medium', 'high', 'urgent'];
|
const VALID_PRIORITIES = ['none', 'low', 'medium', 'high', 'urgent'];
|
||||||
const VALID_STATUSES = ['open', 'in_progress', 'done'];
|
const VALID_STATUSES = ['open', 'in_progress', 'done'];
|
||||||
const VALID_CATEGORIES = ['Haushalt', 'Schule', 'Einkauf', 'Reparatur',
|
const VALID_CATEGORIES = ['Haushalt', 'Schule', 'Einkauf', 'Reparatur',
|
||||||
'Gesundheit', 'Finanzen', 'Freizeit', 'Sonstiges'];
|
'Gesundheit', 'Finanzen', 'Freizeit', 'Sonstiges'];
|
||||||
@@ -96,7 +96,7 @@ router.get('/', (req, res) => {
|
|||||||
ORDER BY
|
ORDER BY
|
||||||
CASE t.status WHEN 'done' THEN 1 ELSE 0 END,
|
CASE t.status WHEN 'done' THEN 1 ELSE 0 END,
|
||||||
CASE t.priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1
|
CASE t.priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1
|
||||||
WHEN 'medium' THEN 2 ELSE 3 END,
|
WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END,
|
||||||
t.due_date ASC NULLS LAST,
|
t.due_date ASC NULLS LAST,
|
||||||
t.created_at DESC
|
t.created_at DESC
|
||||||
`;
|
`;
|
||||||
@@ -148,7 +148,7 @@ router.post('/', (req, res) => {
|
|||||||
title,
|
title,
|
||||||
description = null,
|
description = null,
|
||||||
category = 'Sonstiges',
|
category = 'Sonstiges',
|
||||||
priority = 'medium',
|
priority = 'none',
|
||||||
due_date = null,
|
due_date = null,
|
||||||
due_time = null,
|
due_time = null,
|
||||||
assigned_to = null,
|
assigned_to = null,
|
||||||
|
|||||||
Reference in New Issue
Block a user