From 2d2a989a5f331691f9dc3d739a544631b7201414 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 2 Dec 2025 18:00:36 +0200 Subject: [PATCH] Fix bug 619 (#629) * Add tasks today plan fixes * fixup! Add tasks today plan fixes * fixup! fixup! Add tasks today plan fixes * fixup! fixup! fixup! Add tasks today plan fixes --- backend/database.sqlite | 0 backend/routes/tasks/operations/list.js | 13 +- .../tasks/queries/metrics-computation.js | 41 +- .../routes/tasks/queries/metrics-queries.js | 102 +++- .../routes/tasks/queries/query-builders.js | 1 - .../tests/integration/timezone-fixes.test.js | 16 +- e2e/bin/run-e2e.sh | 29 +- e2e/package-lock.json | 92 ---- e2e/package.json | 16 - e2e/playwright.config.ts | 1 + e2e/tests/today-view.spec.ts | 245 +++++++++ frontend/components/Task/TasksToday.tsx | 514 +++++++++++++----- frontend/components/Task/TodayPlan.tsx | 63 ++- frontend/utils/tasksService.ts | 2 + package-lock.json | 60 ++ package.json | 5 +- public/locales/ar/translation.json | 10 +- public/locales/bg/translation.json | 8 +- public/locales/da/translation.json | 8 +- public/locales/de/translation.json | 8 +- public/locales/el/translation.json | 8 +- public/locales/en/translation.json | 4 +- public/locales/es/translation.json | 8 +- public/locales/fi/translation.json | 8 +- public/locales/fr/translation.json | 8 +- public/locales/id/translation.json | 8 +- public/locales/it/translation.json | 8 +- public/locales/jp/translation.json | 8 +- public/locales/ko/translation.json | 8 +- public/locales/nl/translation.json | 8 +- public/locales/no/translation.json | 8 +- public/locales/pl/translation.json | 8 +- public/locales/pt/translation.json | 8 +- public/locales/ro/translation.json | 8 +- public/locales/ru/translation.json | 8 +- public/locales/sl/translation.json | 8 +- public/locales/sv/translation.json | 8 +- public/locales/tr/translation.json | 8 +- public/locales/ua/translation.json | 8 +- public/locales/vi/translation.json | 8 +- public/locales/zh/translation.json | 8 +- 41 files changed, 1041 insertions(+), 357 deletions(-) create mode 100644 backend/database.sqlite delete mode 100644 e2e/package-lock.json delete mode 100644 e2e/package.json create mode 100644 e2e/tests/today-view.spec.ts diff --git a/backend/database.sqlite b/backend/database.sqlite new file mode 100644 index 0000000..e69de29 diff --git a/backend/routes/tasks/operations/list.js b/backend/routes/tasks/operations/list.js index 8802a9f..276beae 100644 --- a/backend/routes/tasks/operations/list.js +++ b/backend/routes/tasks/operations/list.js @@ -57,18 +57,27 @@ async function addDashboardLists( const listKeys = [ 'tasks_in_progress', + 'tasks_today_plan', 'tasks_due_today', + 'tasks_overdue', 'suggested_tasks', 'tasks_completed_today', ]; + const serializedLists = {}; + for (const key of listKeys) { - response[key] = await serializeTasks( - metricsData[key], + const metricsKey = + key === 'tasks_today_plan' ? 'today_plan_tasks' : key; + serializedLists[key] = await serializeTasks( + metricsData[metricsKey], timezone, serializationOptions ); } + + Object.assign(response, serializedLists); + response.dashboard_lists = serializedLists; } function addPerformanceHeaders(res, startTime, queryStats) { diff --git a/backend/routes/tasks/queries/metrics-computation.js b/backend/routes/tasks/queries/metrics-computation.js index 571ab76..5414797 100644 --- a/backend/routes/tasks/queries/metrics-computation.js +++ b/backend/routes/tasks/queries/metrics-computation.js @@ -8,6 +8,7 @@ const { fetchTasksInProgress, fetchTodayPlanTasks, fetchTasksDueToday, + fetchOverdueTasks, fetchSomedayTaskIds, fetchNonProjectTasks, fetchProjectTasks, @@ -140,6 +141,7 @@ async function computeTaskMetrics( tasksInProgress, todayPlanTasks, tasksDueToday, + tasksOverdue, tasksCompletedToday, weeklyCompletions, ] = await Promise.all([ @@ -148,6 +150,7 @@ async function computeTaskMetrics( fetchTasksInProgress(visibleTasksWhere), fetchTodayPlanTasks(visibleTasksWhere), fetchTasksDueToday(visibleTasksWhere, userTimezone), + fetchOverdueTasks(visibleTasksWhere, userTimezone), fetchTasksCompletedToday(userId, userTimezone), computeWeeklyCompletions(userId, userTimezone), ]); @@ -167,6 +170,7 @@ async function computeTaskMetrics( tasks_in_progress_count: tasksInProgress.length, tasks_in_progress: tasksInProgress, tasks_due_today: tasksDueToday, + tasks_overdue: tasksOverdue, today_plan_tasks: todayPlanTasks, suggested_tasks: suggestedTasks, tasks_completed_today: tasksCompletedToday, @@ -176,8 +180,41 @@ async function computeTaskMetrics( async function getTaskMetrics(userId, timezone) { const metrics = await computeTaskMetrics(userId, timezone); - const { buildMetricsResponse } = require('../core/serializers'); - return await buildMetricsResponse(metrics); + const { + buildMetricsResponse, + serializeTasks, + } = require('../core/serializers'); + + const response = await buildMetricsResponse(metrics); + + const serializedLists = { + tasks_in_progress: await serializeTasks( + metrics.tasks_in_progress, + timezone + ), + tasks_today_plan: await serializeTasks( + metrics.today_plan_tasks, + timezone + ), + tasks_due_today: await serializeTasks( + metrics.tasks_due_today, + timezone + ), + tasks_overdue: await serializeTasks(metrics.tasks_overdue, timezone), + suggested_tasks: await serializeTasks( + metrics.suggested_tasks, + timezone + ), + tasks_completed_today: await serializeTasks( + metrics.tasks_completed_today, + timezone + ), + }; + + Object.assign(response, serializedLists); + response.dashboard_lists = serializedLists; + + return response; } module.exports = { diff --git a/backend/routes/tasks/queries/metrics-queries.js b/backend/routes/tasks/queries/metrics-queries.js index c019ba7..8f43938 100644 --- a/backend/routes/tasks/queries/metrics-queries.js +++ b/backend/routes/tasks/queries/metrics-queries.js @@ -47,18 +47,22 @@ async function fetchTasksInProgress(visibleTasksWhere) { async function fetchTodayPlanTasks(visibleTasksWhere) { return await Task.findAll({ where: { - ...visibleTasksWhere, - today: true, - status: { - [Op.notIn]: [ - Task.STATUS.DONE, - Task.STATUS.ARCHIVED, - 'done', - 'archived', - ], - }, - parent_task_id: null, - recurring_parent_id: null, + [Op.and]: [ + visibleTasksWhere, + { + today: true, + status: { + [Op.notIn]: [ + Task.STATUS.DONE, + Task.STATUS.ARCHIVED, + 'done', + 'archived', + ], + }, + parent_task_id: null, + recurring_parent_id: null, + }, + ], }, include: getTaskIncludeConfig(), order: [ @@ -87,11 +91,20 @@ async function fetchTasksDueToday(visibleTasksWhere, userTimezone) { }, parent_task_id: null, recurring_parent_id: null, + today: { [Op.or]: [false, null] }, [Op.or]: [ - { due_date: { [Op.lte]: todayBounds.end } }, + { + due_date: { + [Op.and]: [ + { [Op.gte]: todayBounds.start }, + { [Op.lte]: todayBounds.end }, + ], + }, + }, sequelize.literal(`EXISTS ( SELECT 1 FROM projects WHERE projects.id = Task.project_id + AND projects.due_date_at >= '${todayBounds.start.toISOString()}' AND projects.due_date_at <= '${todayBounds.end.toISOString()}' )`), ], @@ -99,6 +112,51 @@ async function fetchTasksDueToday(visibleTasksWhere, userTimezone) { ], }, include: getTaskIncludeConfig(), + order: [ + ['priority', 'DESC'], + ['due_date', 'ASC'], + ['project_id', 'ASC'], + ], + }); +} + +async function fetchOverdueTasks(visibleTasksWhere, userTimezone) { + const safeTimezone = getSafeTimezone(userTimezone); + const todayBounds = getTodayBoundsInUTC(safeTimezone); + + return await Task.findAll({ + where: { + [Op.and]: [ + visibleTasksWhere, + { + status: { + [Op.notIn]: [ + Task.STATUS.DONE, + Task.STATUS.ARCHIVED, + 'done', + 'archived', + ], + }, + parent_task_id: null, + recurring_parent_id: null, + today: { [Op.or]: [false, null] }, + [Op.or]: [ + { due_date: { [Op.lt]: todayBounds.start } }, + sequelize.literal(`EXISTS ( + SELECT 1 FROM projects + WHERE projects.id = Task.project_id + AND projects.due_date_at < '${todayBounds.start.toISOString()}' + )`), + ], + }, + ], + }, + include: getTaskIncludeConfig(), + order: [ + ['priority', 'DESC'], + ['due_date', 'ASC'], + ['project_id', 'ASC'], + ], }); } @@ -135,7 +193,8 @@ async function fetchNonProjectTasks( include: getTaskIncludeConfig(), order: [ ['priority', 'DESC'], - ['created_at', 'ASC'], + ['due_date', 'ASC'], + ['project_id', 'ASC'], ], limit: 6, }); @@ -160,7 +219,8 @@ async function fetchProjectTasks( include: getTaskIncludeConfig(), order: [ ['priority', 'DESC'], - ['created_at', 'ASC'], + ['due_date', 'ASC'], + ['project_id', 'ASC'], ], limit: 6, }); @@ -188,16 +248,16 @@ async function fetchSomedayFallbackTasks( include: getTaskIncludeConfig(), order: [ ['priority', 'DESC'], - ['created_at', 'ASC'], + ['due_date', 'ASC'], + ['project_id', 'ASC'], ], limit: limit, }); } async function fetchTasksCompletedToday(userId, userTimezone) { - const todayInUserTz = moment.tz(userTimezone); - const todayStart = todayInUserTz.clone().startOf('day').utc().toDate(); - const todayEnd = todayInUserTz.clone().endOf('day').utc().toDate(); + const safeTimezone = getSafeTimezone(userTimezone); + const todayBounds = getTodayBoundsInUTC(safeTimezone); return await Task.findAll({ where: { @@ -206,7 +266,8 @@ async function fetchTasksCompletedToday(userId, userTimezone) { parent_task_id: null, recurring_parent_id: null, completed_at: { - [Op.between]: [todayStart, todayEnd], + [Op.gte]: todayBounds.start, + [Op.lte]: todayBounds.end, }, }, include: getTaskIncludeConfig(), @@ -220,6 +281,7 @@ module.exports = { fetchTasksInProgress, fetchTodayPlanTasks, fetchTasksDueToday, + fetchOverdueTasks, fetchSomedayTaskIds, fetchNonProjectTasks, fetchProjectTasks, diff --git a/backend/routes/tasks/queries/query-builders.js b/backend/routes/tasks/queries/query-builders.js index bb60cdd..5f3d9d3 100644 --- a/backend/routes/tasks/queries/query-builders.js +++ b/backend/routes/tasks/queries/query-builders.js @@ -131,7 +131,6 @@ async function filterTasksByParams( { recurrence_type: { [Op.ne]: 'none' } }, { recurrence_type: { [Op.ne]: null } }, { recurring_parent_id: null }, - { today: true }, ], }, { diff --git a/backend/tests/integration/timezone-fixes.test.js b/backend/tests/integration/timezone-fixes.test.js index 76ecf82..4e6e7c0 100644 --- a/backend/tests/integration/timezone-fixes.test.js +++ b/backend/tests/integration/timezone-fixes.test.js @@ -225,16 +225,22 @@ describe('Timezone Fixes Integration Tests', () => { expect(tasksRes.statusCode).toBe(200); - // Both tasks should appear in tasks_due_today since they're overdue expect(tasksRes.body.tasks_due_today.length).toBeGreaterThanOrEqual( - 2 + 1 ); - const taskNames = tasksRes.body.tasks_due_today.map( + const dueTodayNames = tasksRes.body.tasks_due_today.map( (task) => task.name ); - expect(taskNames).toContain('Today Task'); - expect(taskNames).toContain('Yesterday Task'); + expect(dueTodayNames).toContain('Today Task'); + expect(tasksRes.body.tasks_overdue.length).toBeGreaterThanOrEqual( + 1 + ); + + const overdueNames = tasksRes.body.tasks_overdue.map( + (task) => task.name + ); + expect(overdueNames).toContain('Yesterday Task'); }); }); diff --git a/e2e/bin/run-e2e.sh b/e2e/bin/run-e2e.sh index 4776054..2a09bcc 100755 --- a/e2e/bin/run-e2e.sh +++ b/e2e/bin/run-e2e.sh @@ -12,26 +12,17 @@ red() { printf "\033[31m%s\033[0m\n" "$*"; } green() { printf "\033[32m%s\033[0m\n" "$*"; } yellow() { printf "\033[33m%s\033[0m\n" "$*"; } -# Ensure dependencies in e2e/ +# Ensure dependencies in root SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" E2E_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" ROOT_DIR="$(cd "$E2E_DIR/.." && pwd)" -cd "$E2E_DIR" -if [ ! -f package.json ]; then - red "e2e/package.json not found" - exit 1 -fi - -# Install e2e deps and browsers -if [ ! -d node_modules ]; then - yellow "Installing e2e dependencies..." - npm ci -fi +cd "$ROOT_DIR" +# Check if Playwright is installed if ! npx playwright --version >/dev/null 2>&1; then yellow "Installing Playwright browsers..." - npm run install-browsers + npx playwright install --with-deps fi # Start backend and frontend @@ -108,8 +99,8 @@ for i in {1..60}; do fi done -# Run tests -cd "$E2E_DIR" +# Run tests (specify config file since we're running from root) +cd "$ROOT_DIR" yellow "Running Playwright tests..." APP_URL="$FRONTEND_URL" \ @@ -117,11 +108,11 @@ E2E_EMAIL="${E2E_EMAIL:-test@tududi.com}" \ E2E_PASSWORD="${E2E_PASSWORD:-password123}" \ bash -c ' if [ "${E2E_MODE:-}" = "ui" ]; then - npm run test:ui + npx playwright test --ui --config=e2e/playwright.config.ts elif [ "${E2E_MODE:-}" = "headed" ]; then - # Respect E2E_SLOWMO and run only Firefox sequentially - npx playwright test --headed --project=Firefox --workers=1 + # Respect E2E_SLOWMO and run only Chromium sequentially + npx playwright test --headed --project=Chromium --workers=1 --config=e2e/playwright.config.ts else - npx playwright test --workers=5 + npx playwright test --config=e2e/playwright.config.ts fi ' diff --git a/e2e/package-lock.json b/e2e/package-lock.json deleted file mode 100644 index ecadb8c..0000000 --- a/e2e/package-lock.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "name": "tududi-e2e", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "tududi-e2e", - "version": "0.1.0", - "devDependencies": { - "@playwright/test": "^1.47.2", - "dotenv": "^16.5.0" - } - }, - "node_modules/@playwright/test": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz", - "integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.54.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz", - "integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.54.2" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.54.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz", - "integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - } - } -} diff --git a/e2e/package.json b/e2e/package.json deleted file mode 100644 index 66b96d9..0000000 --- a/e2e/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "tududi-e2e", - "private": true, - "version": "0.1.0", - "description": "End-to-end tests for tududi using Playwright", - "scripts": { - "test": "playwright test", - "test:ui": "playwright test --ui", - "codegen": "playwright codegen ${APP_URL:-http://localhost:8080}", - "install-browsers": "playwright install --with-deps" - }, - "devDependencies": { - "@playwright/test": "^1.47.2", - "dotenv": "^16.5.0" - } -} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index db15069..39f25d5 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ timeout: 60_000, expect: { timeout: 10_000 }, fullyParallel: true, + workers: process.env.CI ? 1 : undefined, // Use default workers locally, 1 in CI reporter: [['list']], use: { baseURL, diff --git a/e2e/tests/today-view.spec.ts b/e2e/tests/today-view.spec.ts new file mode 100644 index 0000000..7bfed5e --- /dev/null +++ b/e2e/tests/today-view.spec.ts @@ -0,0 +1,245 @@ +import { test, expect } from '@playwright/test'; + +test.describe.serial('Today View', () => { + // Helper function to login via UI + async function loginViaUI(page, baseURL) { + const appUrl = + baseURL ?? process.env.APP_URL ?? 'http://localhost:8080'; + await page.goto(`${appUrl}/login`); + + await page.fill( + 'input[type="email"]', + process.env.E2E_EMAIL || 'test@tududi.com' + ); + await page.fill( + 'input[type="password"]', + process.env.E2E_PASSWORD || 'password123' + ); + await page.click('button[type="submit"]'); + + // Wait for redirect to dashboard or today page + await page.waitForURL(/\/(dashboard|today)/, { timeout: 10000 }); + } + + test('should only show tasks with today flag in Planned section', async ({ + page, + context, + baseURL, + }) => { + // Login first + await loginViaUI(page, baseURL); + + const appUrl = + baseURL ?? process.env.APP_URL ?? 'http://localhost:8080'; + const timestamp = Date.now(); + + // Create tasks via API using the logged-in context + const tasksToCreate = [ + { + name: `High Priority Planned ${timestamp}`, + today: true, + priority: 2, + }, // 2 = HIGH + { + name: `Task Without Today Flag ${timestamp}`, + today: false, + priority: 2, + }, // 2 = HIGH + ]; + + const taskIds: string[] = []; + const createdTasks: any[] = []; + + for (const taskData of tasksToCreate) { + const response = await context.request.post(`${appUrl}/api/task`, { + data: taskData, + }); + + if (response.ok()) { + const task = await response.json(); + taskIds.push(task.id); + createdTasks.push(task); + } + } + + // Navigate to today page and wait for metrics to load + await page.goto(`${appUrl}/today`); + await page.waitForLoadState('networkidle'); + + // Check if Planned section exists using data-testid + const plannedSection = page.getByTestId('planned-section'); + await expect(plannedSection).toBeVisible({ timeout: 10000 }); + + // Verify task with today flag is visible in the Planned section + const withTodayFlagTask = plannedSection.getByTestId( + `task-item-${taskIds[0]}` + ); + await expect(withTodayFlagTask).toBeVisible({ timeout: 10000 }); + + // Verify task without today flag is NOT visible in Planned section + const withoutFlagTask = plannedSection.getByTestId( + `task-item-${taskIds[1]}` + ); + await expect(withoutFlagTask).not.toBeVisible(); + + // Clean up created tasks + for (const taskId of taskIds) { + await context.request.delete(`${appUrl}/api/task/${taskId}`); + } + }); + + test('should show overdue tasks in Overdue section', async ({ + page, + context, + baseURL, + }) => { + // Login first + await loginViaUI(page, baseURL); + + const appUrl = + baseURL ?? process.env.APP_URL ?? 'http://localhost:8080'; + const timestamp = Date.now(); + + // Calculate yesterday's date + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const yesterdayStr = yesterday.toISOString().split('T')[0]; + + // Create an overdue task + const response = await context.request.post(`${appUrl}/api/task`, { + data: { + name: `Overdue Task ${timestamp}`, + due_date: yesterdayStr, + priority: 2, + }, // 2 = HIGH + }); + + let taskId: string | null = null; + if (response.ok()) { + const task = await response.json(); + taskId = task.id; + } + + // Navigate to today page + await page.goto(`${appUrl}/today`); + await page.waitForLoadState('networkidle'); + + // Wait a bit for React to render the sections + await page.waitForTimeout(2000); + + // Check if Overdue section exists using data-testid + const overdueSection = page.getByTestId('overdue-section'); + const isOverdueVisible = await overdueSection + .isVisible() + .catch(() => false); + + // If overdue section is visible, verify the task is in it + if (isOverdueVisible) { + const overdueTask = overdueSection.getByTestId( + `task-item-${taskId}` + ); + await expect(overdueTask).toBeVisible(); + } else { + // If section not visible, the settings might be hiding it + // Skip this assertion but don't fail the test + console.log( + 'Overdue section not visible - may be hidden by settings' + ); + } + + // Clean up + if (taskId) { + await context.request.delete(`${appUrl}/api/task/${taskId}`); + } + }); + + test('should show tasks due today in Due Today section', async ({ + page, + context, + baseURL, + }) => { + // Login first + await loginViaUI(page, baseURL); + + const appUrl = + baseURL ?? process.env.APP_URL ?? 'http://localhost:8080'; + const timestamp = Date.now(); + + // Calculate today's date + const today = new Date(); + const todayStr = today.toISOString().split('T')[0]; + + // Create a task due today (but not in today plan) + const response = await context.request.post(`${appUrl}/api/task`, { + data: { + name: `Due Today Task ${timestamp}`, + due_date: todayStr, + priority: 2, + today: false, + }, // 2 = HIGH + }); + + let taskId: string | null = null; + if (response.ok()) { + const task = await response.json(); + taskId = task.id; + } + + // Navigate to today page + await page.goto(`${appUrl}/today`); + await page.waitForLoadState('networkidle'); + + // Wait a bit for React to render the sections + await page.waitForTimeout(2000); + + // Check if Due Today section exists using data-testid + const dueTodaySection = page.getByTestId('due-today-section'); + const isDueTodayVisible = await dueTodaySection + .isVisible() + .catch(() => false); + + // If due today section is visible, verify the task is in it + if (isDueTodayVisible) { + const dueTodayTask = dueTodaySection.getByTestId( + `task-item-${taskId}` + ); + await expect(dueTodayTask).toBeVisible(); + } else { + // If section not visible, the settings might be hiding it + console.log( + 'Due Today section not visible - may be hidden by settings' + ); + } + + // Clean up + if (taskId) { + await context.request.delete(`${appUrl}/api/task/${taskId}`); + } + }); + + test('should allow collapsing and expanding sections', async ({ + page, + baseURL, + }) => { + // Login first + await loginViaUI(page, baseURL); + + const appUrl = + baseURL ?? process.env.APP_URL ?? 'http://localhost:8080'; + await page.goto(`${appUrl}/today`); + await page.waitForLoadState('networkidle'); + + // Test Planned section collapse/expand if it exists using data-testid + const plannedHeader = page.getByTestId('planned-section-header'); + const isPlannedVisible = await plannedHeader + .isVisible({ timeout: 5000 }) + .catch(() => false); + + if (isPlannedVisible) { + // Verify header is clickable + await expect(plannedHeader).toBeVisible(); + // This test just verifies the section exists and is clickable + // Actual collapse/expand behavior depends on having tasks + } + }); +}); diff --git a/frontend/components/Task/TasksToday.tsx b/frontend/components/Task/TasksToday.tsx index a305611..4d58947 100644 --- a/frontend/components/Task/TasksToday.tsx +++ b/frontend/components/Task/TasksToday.tsx @@ -93,6 +93,18 @@ const TasksToday: React.FC = () => { const stored = localStorage.getItem('completedTasksCollapsed'); return stored === 'true'; }); + const [isOverdueCollapsed, setIsOverdueCollapsed] = useState(() => { + const stored = localStorage.getItem('overdueTasksCollapsed'); + return stored === 'true'; + }); + const [isTodayPlanCollapsed, setIsTodayPlanCollapsed] = useState(() => { + const stored = localStorage.getItem('todayPlanTasksCollapsed'); + return stored === 'true'; + }); + const [isDueTodayCollapsed, setIsDueTodayCollapsed] = useState(() => { + const stored = localStorage.getItem('dueTodayTasksCollapsed'); + return stored === 'true'; + }); const [isSettingsLoaded, setIsSettingsLoaded] = useState(false); // Metrics from the API (counts) + task arrays stored locally @@ -100,6 +112,7 @@ const TasksToday: React.FC = () => { Metrics & { tasks_in_progress?: Task[]; tasks_due_today?: Task[]; + tasks_overdue?: Task[]; today_plan_tasks?: Task[]; suggested_tasks?: Task[]; tasks_completed_today?: Task[]; @@ -116,6 +129,7 @@ const TasksToday: React.FC = () => { // Task arrays (fetched separately via include_lists parameter) tasks_in_progress: [], tasks_due_today: [], + tasks_overdue: [], today_plan_tasks: [], suggested_tasks: [], tasks_completed_today: [], @@ -132,6 +146,9 @@ const TasksToday: React.FC = () => { // Client-side pagination for Due Today tasks (since backend returns all) const [dueTodayDisplayLimit, setDueTodayDisplayLimit] = useState(20); + // Client-side pagination for Overdue tasks (since backend returns all) + const [overdueDisplayLimit, setOverdueDisplayLimit] = useState(20); + // Client-side pagination for Completed Today tasks (since backend returns all) const [completedTodayDisplayLimit, setCompletedTodayDisplayLimit] = useState(20); @@ -220,6 +237,24 @@ const TasksToday: React.FC = () => { localStorage.setItem('completedTasksCollapsed', newState.toString()); }; + const toggleOverdueCollapsed = () => { + const newState = !isOverdueCollapsed; + setIsOverdueCollapsed(newState); + localStorage.setItem('overdueTasksCollapsed', newState.toString()); + }; + + const toggleTodayPlanCollapsed = () => { + const newState = !isTodayPlanCollapsed; + setIsTodayPlanCollapsed(newState); + localStorage.setItem('todayPlanTasksCollapsed', newState.toString()); + }; + + const toggleDueTodayCollapsed = () => { + const newState = !isDueTodayCollapsed; + setIsDueTodayCollapsed(newState); + localStorage.setItem('dueTodayTasksCollapsed', newState.toString()); + }; + // Load data once on component mount useEffect(() => { isMounted.current = true; @@ -241,6 +276,7 @@ const TasksToday: React.FC = () => { // Store task arrays locally (fetched via include_lists=true) tasks_in_progress: result.tasks_in_progress || [], tasks_due_today: result.tasks_due_today || [], + tasks_overdue: result.tasks_overdue || [], today_plan_tasks: result.tasks || [], // Main tasks array is today plan suggested_tasks: result.suggested_tasks || [], tasks_completed_today: @@ -511,6 +547,9 @@ const TasksToday: React.FC = () => { newMetrics.tasks_due_today = removeTask( newMetrics.tasks_due_today || [] ); + newMetrics.tasks_overdue = removeTask( + newMetrics.tasks_overdue || [] + ); newMetrics.tasks_in_progress = removeTask( newMetrics.tasks_in_progress || [] ); @@ -553,13 +592,9 @@ const TasksToday: React.FC = () => { updatedTask ); } - // Check if due today (and not already in today_plan_tasks or in_progress) - const isDueToday = - updatedTask.due_date && - format(new Date(updatedTask.due_date), 'yyyy-MM-dd') === - format(new Date(), 'yyyy-MM-dd'); + // Check if task has a due date (and not already in today_plan_tasks or in_progress) if ( - isDueToday && + updatedTask.due_date && updatedTask.status !== 'archived' && !newMetrics.today_plan_tasks.some( (t) => t.id === updatedTask.id @@ -568,10 +603,26 @@ const TasksToday: React.FC = () => { (t) => t.id === updatedTask.id ) ) { - newMetrics.tasks_due_today = updateOrAddTask( - newMetrics.tasks_due_today, - updatedTask + const today = new Date(); + const todayStr = format(today, 'yyyy-MM-dd'); + const dueDateStr = format( + new Date(updatedTask.due_date), + 'yyyy-MM-dd' ); + + if (dueDateStr === todayStr) { + // Due today + newMetrics.tasks_due_today = updateOrAddTask( + newMetrics.tasks_due_today, + updatedTask + ); + } else if (dueDateStr < todayStr) { + // Overdue + newMetrics.tasks_overdue = updateOrAddTask( + newMetrics.tasks_overdue, + updatedTask + ); + } } // Check for suggested tasks (and not already in other active lists) const isSuggested = @@ -611,6 +662,7 @@ const TasksToday: React.FC = () => { newMetrics.today_plan_tasks.length + newMetrics.suggested_tasks.length + newMetrics.tasks_due_today.length + + newMetrics.tasks_overdue.length + newMetrics.tasks_in_progress.length; return newMetrics; @@ -670,6 +722,11 @@ const TasksToday: React.FC = () => { newMetrics.tasks_due_today ); } + if (newMetrics.tasks_overdue) { + newMetrics.tasks_overdue = updateTaskInList( + newMetrics.tasks_overdue + ); + } if (newMetrics.tasks_in_progress) { newMetrics.tasks_in_progress = updateTaskInList( newMetrics.tasks_in_progress @@ -712,6 +769,7 @@ const TasksToday: React.FC = () => { ...result.metrics, tasks_in_progress: result.tasks_in_progress || [], tasks_due_today: result.tasks_due_today || [], + tasks_overdue: result.tasks_overdue || [], today_plan_tasks: result.tasks || [], suggested_tasks: result.suggested_tasks || [], tasks_completed_today: @@ -740,6 +798,7 @@ const TasksToday: React.FC = () => { ...result.metrics, tasks_in_progress: result.tasks_in_progress || [], tasks_due_today: result.tasks_due_today || [], + tasks_overdue: result.tasks_overdue || [], today_plan_tasks: result.tasks || [], suggested_tasks: result.suggested_tasks || [], tasks_completed_today: @@ -798,6 +857,7 @@ const TasksToday: React.FC = () => { ...result.metrics, tasks_in_progress: result.tasks_in_progress || [], tasks_due_today: result.tasks_due_today || [], + tasks_overdue: result.tasks_overdue || [], today_plan_tasks: result.tasks || [], suggested_tasks: result.suggested_tasks || [], tasks_completed_today: @@ -817,6 +877,10 @@ const TasksToday: React.FC = () => { ...(prevMetrics.tasks_due_today || []), ...(result.tasks_due_today || []), ], + tasks_overdue: [ + ...(prevMetrics.tasks_overdue || []), + ...(result.tasks_overdue || []), + ], today_plan_tasks: [ ...(prevMetrics.today_plan_tasks || []), ...(result.tasks || []), @@ -1200,80 +1264,309 @@ const TasksToday: React.FC = () => { ) : null} - {/* Today Plan */} - + {/* Overdue Tasks - Displayed first */} + {isSettingsLoaded && + todaySettings.showDueToday && + metrics.tasks_overdue.length > 0 && ( +
+
+

+ {t('tasks.overdue', 'Overdue')} +

+
+ + {metrics.tasks_overdue.length} + + {isOverdueCollapsed ? ( + + ) : ( + + )} +
+
+ {!isOverdueCollapsed && ( + <> + - {/* Load More Buttons for Today Plan Tasks */} - {pagination.hasMore && ( -
- + +
+ )} + + {/* Pagination info for Overdue tasks */} +
+ {t( + 'tasks.showingItems', + 'Showing {{current}} of {{total}} items', + { + current: Math.min( + overdueDisplayLimit, + metrics.tasks_overdue.length + ), + total: metrics.tasks_overdue + .length, + } + )} +
)} - - -
- )} + + )} - {/* Pagination info for Today Plan tasks */} + {/* Today Plan */} {(metrics.today_plan_tasks || []).length > 0 && ( -
- {t( - 'tasks.showingItems', - 'Showing {{current}} of {{total}} items', - { - current: (metrics.today_plan_tasks || []) - .length, - total: pagination.total, - } +
+
+

+ {t('tasks.planned', 'Planned')} +

+
+ + {(metrics.today_plan_tasks || []).length} + + {isTodayPlanCollapsed ? ( + + ) : ( + + )} +
+
+ {!isTodayPlanCollapsed && ( + <> + + + {/* Load More Buttons for Today Plan Tasks */} + {pagination.hasMore && ( +
+ + +
+ )} + + {/* Pagination info for Today Plan tasks */} +
+ {t( + 'tasks.showingItems', + 'Showing {{current}} of {{total}} items', + { + current: ( + metrics.today_plan_tasks || [] + ).length, + total: pagination.total, + } + )} +
+ )}
)} + {/* Due Today Tasks */} + {isSettingsLoaded && + todaySettings.showDueToday && + metrics.tasks_due_today.length > 0 && ( +
+
+

+ {t('tasks.dueToday')} +

+
+ + {metrics.tasks_due_today.length} + + {isDueTodayCollapsed ? ( + + ) : ( + + )} +
+
+ {!isDueTodayCollapsed && ( + <> + + + {/* Load More Buttons for Due Today Tasks */} + {dueTodayDisplayLimit < + metrics.tasks_due_today.length && ( +
+ + +
+ )} + + {/* Pagination info for Due Today tasks */} +
+ {t( + 'tasks.showingItems', + 'Showing {{current}} of {{total}} items', + { + current: Math.min( + dueTodayDisplayLimit, + metrics.tasks_due_today + .length + ), + total: metrics.tasks_due_today + .length, + } + )} +
+ + )} +
+ )} + {/* Suggested Tasks - Separate setting */} {!isSettingsLoaded ? ( // Invisible placeholder for suggestions @@ -1319,83 +1612,20 @@ const TasksToday: React.FC = () => {
) : null} - {/* Due Today Tasks - Conditionally Rendered */} - {isSettingsLoaded && - todaySettings.showDueToday && - metrics.tasks_due_today.length > 0 && ( -
-

- {t('tasks.dueToday')} -

- - - {/* Load More Buttons for Due Today Tasks */} - {dueTodayDisplayLimit < - metrics.tasks_due_today.length && ( -
- - -
- )} - - {/* Pagination info for Due Today tasks */} -
- {t( - 'tasks.showingItems', - 'Showing {{current}} of {{total}} items', - { - current: Math.min( - dueTodayDisplayLimit, - metrics.tasks_due_today.length - ), - total: metrics.tasks_due_today.length, - } - )} -
-
- )} - {/* Completed Tasks - Conditionally Rendered */} {isSettingsLoaded && todaySettings.showCompleted && (() => { const completedToday = metrics.tasks_completed_today; // Use the already filtered list from backend return ( -
+

{t('tasks.completedToday')} diff --git a/frontend/components/Task/TodayPlan.tsx b/frontend/components/Task/TodayPlan.tsx index e01b35a..36178ba 100644 --- a/frontend/components/Task/TodayPlan.tsx +++ b/frontend/components/Task/TodayPlan.tsx @@ -35,17 +35,66 @@ const TodayPlan: React.FC = ({ const aInProgress = a.status === 'in_progress' || a.status === 1; const bInProgress = b.status === 'in_progress' || b.status === 1; - // If both are in progress, sort by updated_at (recently updated to bottom) + // If both are in progress, sort by multi-criteria if (aInProgress && bInProgress) { - // Recently updated tasks should be at the bottom of in-progress group - const aUpdated = new Date(a.updated_at || a.created_at || 0); - const bUpdated = new Date(b.updated_at || b.created_at || 0); - return aUpdated.getTime() - bUpdated.getTime(); // Older tasks first, newer to bottom + // 1. Priority (High → Medium → Low → None) + const priorityOrder = { high: 3, medium: 2, low: 1 }; + const aPriority = + priorityOrder[a.priority as keyof typeof priorityOrder] || + 0; + const bPriority = + priorityOrder[b.priority as keyof typeof priorityOrder] || + 0; + if (aPriority !== bPriority) { + return bPriority - aPriority; // Higher priority first + } + + // 2. Due date (earlier first, null/undefined last) + const aDueDate = a.due_date + ? new Date(a.due_date).getTime() + : Infinity; + const bDueDate = b.due_date + ? new Date(b.due_date).getTime() + : Infinity; + if (aDueDate !== bDueDate) { + return aDueDate - bDueDate; + } + + // 3. Project (tasks with same priority and due date grouped by project) + const aProject = a.project_id || ''; + const bProject = b.project_id || ''; + return aProject.toString().localeCompare(bProject.toString()); } - // If both are not in progress, maintain original order + // If both are not in progress, sort by multi-criteria if (!aInProgress && !bInProgress) { - return 0; + // 1. Priority (High → Medium → Low → None) + const priorityOrder = { high: 3, medium: 2, low: 1 }; + const aPriority = + priorityOrder[a.priority as keyof typeof priorityOrder] || + 0; + const bPriority = + priorityOrder[b.priority as keyof typeof priorityOrder] || + 0; + if (aPriority !== bPriority) { + return bPriority - aPriority; // Higher priority first + } + + // 2. Due date (earlier first, null/undefined last) + const aDueDate = a.due_date + ? new Date(a.due_date).getTime() + : Infinity; + const bDueDate = b.due_date + ? new Date(b.due_date).getTime() + : Infinity; + if (aDueDate !== bDueDate) { + return aDueDate - bDueDate; + } + + // 3. Project (tasks with same priority and due date grouped by project) + const aProject = a.project_id || ''; + const bProject = b.project_id || ''; + return aProject.toString().localeCompare(bProject.toString()); } // Put in-progress tasks first diff --git a/frontend/utils/tasksService.ts b/frontend/utils/tasksService.ts index d4b9a23..dde86f5 100644 --- a/frontend/utils/tasksService.ts +++ b/frontend/utils/tasksService.ts @@ -19,6 +19,7 @@ export const fetchTasks = async ( groupedTasks?: GroupedTasks; tasks_in_progress?: Task[]; tasks_due_today?: Task[]; + tasks_overdue?: Task[]; suggested_tasks?: Task[]; tasks_completed_today?: Task[]; pagination?: { @@ -64,6 +65,7 @@ export const fetchTasks = async ( // Dashboard task lists (only present when include_lists=true) tasks_in_progress: tasksResult.tasks_in_progress, tasks_due_today: tasksResult.tasks_due_today, + tasks_overdue: tasksResult.tasks_overdue, suggested_tasks: tasksResult.suggested_tasks, tasks_completed_today: tasksResult.tasks_completed_today, // Pagination metadata diff --git a/package-lock.json b/package-lock.json index 872f0d6..f5a4e4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "v0.87-beta.2", "license": "ISC", "dependencies": { + "@playwright/test": "^1.57.0", "bcrypt": "~6.0.0", "compression": "~1.8.0", "compromise": "^14.14.4", @@ -3213,6 +3214,21 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz", @@ -15267,6 +15283,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index cf19ba4..cf2b1c1 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test": "npm run backend:test", "test:backend": "npm run backend:test", "test:ui": "bash e2e/bin/run-e2e.sh && echo \"Success!\"", + "test:ui:mode": "cross-env E2E_MODE=ui bash e2e/bin/run-e2e.sh", "test:ui:headed": "cross-env E2E_MODE=headed E2E_SLOWMO=500 bash e2e/bin/run-e2e.sh", "test:watch": "npm run frontend:test:watch", "test:coverage": "npm run frontend:test:coverage && npm run backend:test:coverage", @@ -56,7 +57,8 @@ "lint:fix": "npm run frontend:lint:fix && npm run backend:lint:fix", "format": "npm run frontend:format && npm run backend:format", "format:fix": "npm run frontend:format:fix && npm run backend:format:fix", - "docker:test-build": "bash scripts/test-docker-build.sh" + "docker:test-build": "bash scripts/test-docker-build.sh", + "kill:all": "lsof -ti:8080,3002 | xargs kill -9 2>/dev/null || true" }, "keywords": [], "author": "", @@ -132,6 +134,7 @@ "zustand": "^5.0.3" }, "dependencies": { + "@playwright/test": "^1.57.0", "bcrypt": "~6.0.0", "compression": "~1.8.0", "compromise": "^14.14.4", diff --git a/public/locales/ar/translation.json b/public/locales/ar/translation.json index 8f24ff5..f92f096 100644 --- a/public/locales/ar/translation.json +++ b/public/locales/ar/translation.json @@ -103,7 +103,7 @@ "dueToday": "مستحقة اليوم", "stale": "قديمة", "suggested": "مقترحة", - "completedToday": "مكتمل اليوم", + "completedToday": "مكتمل", "weeklyCompletions": "التقدم الأسبوعي", "taskCompleted": "تم إكمال المهمة", "tasksCompleted": "تم إكمال المهام", @@ -140,7 +140,9 @@ "noProject": "بدون مشروع", "unknownProject": "مشروع غير معروف", "tasks": "مهام", - "showingItems": "عرض {{current}} من {{total}} عنصر" + "showingItems": "عرض {{current}} من {{total}} عنصر", + "overdue": "متأخر", + "planned": "مخطط" }, "timeline": { "activityTimeline": "جدول الأنشطة", @@ -782,7 +784,9 @@ "planned_desc": "تم تحديد نطاقها وجاهزة للبدء", "in_progress_desc": "يتم العمل النشط", "blocked_desc": "مؤقتًا متوقف أو عالق", - "completed_desc": "تم الانتهاء منها" + "completed_desc": "تم الانتهاء منها", + "active": "قيد التنفيذ", + "active_desc": "عمل نشط جارٍ" }, "showMetrics": "عرض المقاييس", "hideMetrics": "إخفاء المقاييس" diff --git a/public/locales/bg/translation.json b/public/locales/bg/translation.json index f3b4de7..73b4e89 100644 --- a/public/locales/bg/translation.json +++ b/public/locales/bg/translation.json @@ -140,7 +140,9 @@ "noProject": "Без проект", "unknownProject": "Неизвестен проект", "tasks": "задачи", - "showingItems": "Показване на {{current}} от {{total}} елемента" + "showingItems": "Показване на {{current}} от {{total}} елемента", + "overdue": "Просрочено", + "planned": "Планирано" }, "timeline": { "activityTimeline": "Хронология на активността", @@ -782,7 +784,9 @@ "planned_desc": "Определено и готово за стартиране", "in_progress_desc": "Активна работа в ход", "blocked_desc": "Временно спряно или блокирано", - "completed_desc": "Завършено и готово" + "completed_desc": "Завършено и готово", + "active": "В процес", + "active_desc": "Активна работа в ход" }, "showMetrics": "Покажи метрики", "hideMetrics": "Скрий метрики" diff --git a/public/locales/da/translation.json b/public/locales/da/translation.json index deae877..d043e92 100644 --- a/public/locales/da/translation.json +++ b/public/locales/da/translation.json @@ -140,7 +140,9 @@ "noProject": "Intet projekt", "unknownProject": "Ukendt projekt", "tasks": "opgaver", - "showingItems": "Viser {{current}} af {{total}} elementer" + "showingItems": "Viser {{current}} af {{total}} elementer", + "overdue": "Forsinket", + "planned": "Planlagt" }, "timeline": { "activityTimeline": "Aktivitets Tidslinje", @@ -782,7 +784,9 @@ "planned_desc": "Afgrænset og klar til at starte", "in_progress_desc": "Aktivt arbejde i gang", "blocked_desc": "Midlertidigt pauseret eller fastlåst", - "completed_desc": "Afsluttet og færdig" + "completed_desc": "Afsluttet og færdig", + "active": "I gang", + "active_desc": "Aktivt arbejde i gang" }, "showMetrics": "Vis målinger", "hideMetrics": "Skjul målinger" diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index f34d88b..5945e48 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -99,8 +99,10 @@ "title": "Aufgaben", "backlog": "Backlog", "inProgress": "In Bearbeitung", + "overdue": "Überfällig", + "planned": "Geplant", "dueToday": "Heute fällig", - "completedToday": "Heute abgeschlossen", + "completedToday": "Abgeschlossen", "myPlanToday": "Mein Plan für Heute", "noPlanToday": "Noch keine Aufgaben für heute geplant", "addToPlanHint": "Verwende die Kalendersymbole neben den Aufgaben, um sie zu deinem heutigen Plan hinzuzufügen", @@ -872,7 +874,9 @@ "planned_desc": "Definiert und bereit zum Start", "in_progress_desc": "Aktive Arbeit im Gange", "blocked_desc": "Vorübergehend pausiert oder festgefahren", - "completed_desc": "Fertiggestellt und abgeschlossen" + "completed_desc": "Fertiggestellt und abgeschlossen", + "active": "In Bearbeitung", + "active_desc": "Aktive Arbeit im Gange" }, "showMetrics": "Metriken anzeigen", "hideMetrics": "Metriken ausblenden" diff --git a/public/locales/el/translation.json b/public/locales/el/translation.json index e9cddbf..0b3a147 100644 --- a/public/locales/el/translation.json +++ b/public/locales/el/translation.json @@ -350,7 +350,9 @@ "noProject": "Χωρίς έργο", "unknownProject": "Άγνωστο έργο", "tasks": "εργασίες", - "showingItems": "Εμφάνιση {{current}} από {{total}} στοιχεία" + "showingItems": "Εμφάνιση {{current}} από {{total}} στοιχεία", + "overdue": "Υπερβολικό", + "planned": "Προγραμματισμένο" }, "timeline": { "activityTimeline": "Χρονοδιάγραμμα Δραστηριότητας", @@ -420,7 +422,9 @@ "planned_desc": "Καθορισμένο και έτοιμο να ξεκινήσει", "in_progress_desc": "Ενεργή εργασία σε εξέλιξη", "blocked_desc": "Προσωρινά παγωμένο ή κολλημένο", - "completed_desc": "Ολοκληρώθηκε και τελείωσε" + "completed_desc": "Ολοκληρώθηκε και τελείωσε", + "active": "Σε Εξέλιξη", + "active_desc": "Ενεργή εργασία που πραγματοποιείται" }, "showMetrics": "Εμφάνιση μετρήσεων", "hideMetrics": "Απόκρυψη μετρήσεων" diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 05d1112..5f74afc 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -100,10 +100,12 @@ "today": "Today", "backlog": "Backlog", "inProgress": "In Progress", + "overdue": "Overdue", + "planned": "Planned", "dueToday": "Due Today", "stale": "Stale", "suggested": "Suggested", - "completedToday": "Completed Today", + "completedToday": "Completed", "weeklyCompletions": "Weekly Progress", "taskCompleted": "task completed", "tasksCompleted": "tasks completed", diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index 1c56f90..a99f059 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -310,10 +310,12 @@ "today": "Hoy", "backlog": "Pendientes", "inProgress": "En Progreso", + "overdue": "Vencidas", + "planned": "Planificadas", "dueToday": "Vence Hoy", "stale": "Atrasados", "suggested": "Sugeridos", - "completedToday": "Completadas Hoy", + "completedToday": "Completadas", "noTasksAvailable": "No hay tareas disponibles para hoy.", "searchPlaceholder": "Buscar tareas...", "addNewTask": "Añadir Nueva Tarea", @@ -420,7 +422,9 @@ "planned_desc": "Definido y listo para comenzar", "in_progress_desc": "Trabajo activo en curso", "blocked_desc": "Pausado temporalmente o atascado", - "completed_desc": "Terminado y completado" + "completed_desc": "Terminado y completado", + "active": "En Progreso", + "active_desc": "Trabajo activo en curso" }, "showMetrics": "Mostrar métricas", "hideMetrics": "Ocultar métricas" diff --git a/public/locales/fi/translation.json b/public/locales/fi/translation.json index d4e556b..fb327e9 100644 --- a/public/locales/fi/translation.json +++ b/public/locales/fi/translation.json @@ -140,7 +140,9 @@ "noProject": "Ei projektia", "unknownProject": "Tuntematon projekti", "tasks": "tehtävät", - "showingItems": "Näytetään {{current}} / {{total}} kohdetta" + "showingItems": "Näytetään {{current}} / {{total}} kohdetta", + "overdue": "Erääntynyt", + "planned": "Suunniteltu" }, "timeline": { "activityTimeline": "Toiminta-aikajana", @@ -782,7 +784,9 @@ "planned_desc": "Määritelty ja valmis aloittamaan", "in_progress_desc": "Aktiivista työtä käynnissä", "blocked_desc": "Tilapäisesti keskeytetty tai jumissa", - "completed_desc": "Valmistunut ja tehty" + "completed_desc": "Valmistunut ja tehty", + "active": "Käynnissä", + "active_desc": "Aktiivista työtä tapahtuu" }, "showMetrics": "Näytä mittarit", "hideMetrics": "Piilota mittarit" diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 7fef813..60e844c 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -100,10 +100,12 @@ "today": "Aujourd'hui", "backlog": "Retard", "inProgress": "En Cours", + "overdue": "En Retard", + "planned": "Planifié", "dueToday": "Échéance Aujourd'hui", "stale": "Obsolète", "suggested": "Suggéré", - "completedToday": "Terminé Aujourd'hui", + "completedToday": "Terminé", "weeklyCompletions": "Progrès Hebdomadaire", "taskCompleted": "tâche terminée", "tasksCompleted": "tâches terminées", @@ -782,7 +784,9 @@ "planned_desc": "Défini et prêt à commencer", "in_progress_desc": "Travail actif en cours", "blocked_desc": "Temporairement mis en pause ou bloqué", - "completed_desc": "Fini et terminé" + "completed_desc": "Fini et terminé", + "active": "En cours", + "active_desc": "Travail actif en cours" }, "showMetrics": "Afficher les métriques", "hideMetrics": "Masquer les métriques" diff --git a/public/locales/id/translation.json b/public/locales/id/translation.json index 1aa5d3d..1db39f6 100644 --- a/public/locales/id/translation.json +++ b/public/locales/id/translation.json @@ -140,7 +140,9 @@ "noProject": "Tanpa proyek", "unknownProject": "Proyek tidak dikenal", "tasks": "tugas", - "showingItems": "Menampilkan {{current}} dari {{total}} item" + "showingItems": "Menampilkan {{current}} dari {{total}} item", + "overdue": "Terlambat", + "planned": "Direncanakan" }, "timeline": { "activityTimeline": "Garis Waktu Aktivitas", @@ -782,7 +784,9 @@ "planned_desc": "Sudah ditentukan dan siap untuk dimulai", "in_progress_desc": "Pekerjaan aktif sedang berlangsung", "blocked_desc": "Sementara terhenti atau terjebak", - "completed_desc": "Selesai dan selesai" + "completed_desc": "Selesai dan selesai", + "active": "Sedang Berlangsung", + "active_desc": "Pekerjaan aktif sedang berlangsung" }, "showMetrics": "Tampilkan metrik", "hideMetrics": "Sembunyikan metrik" diff --git a/public/locales/it/translation.json b/public/locales/it/translation.json index 242c8c7..eb60b2b 100644 --- a/public/locales/it/translation.json +++ b/public/locales/it/translation.json @@ -140,7 +140,9 @@ "noProject": "Nessun progetto", "unknownProject": "Progetto sconosciuto", "tasks": "attività", - "showingItems": "Visualizzazione di {{current}} su {{total}} elementi" + "showingItems": "Visualizzazione di {{current}} su {{total}} elementi", + "overdue": "Scaduto", + "planned": "Pianificato" }, "timeline": { "activityTimeline": "Timeline delle Attività", @@ -782,7 +784,9 @@ "planned_desc": "Definito e pronto per iniziare", "in_progress_desc": "Lavoro attivo in corso", "blocked_desc": "Pausa temporanea o bloccato", - "completed_desc": "Finito e completato" + "completed_desc": "Finito e completato", + "active": "In Corso", + "active_desc": "Lavoro attivo in corso" }, "showMetrics": "Mostra metriche", "hideMetrics": "Nascondi metriche" diff --git a/public/locales/jp/translation.json b/public/locales/jp/translation.json index 62d6a94..82f3ba7 100644 --- a/public/locales/jp/translation.json +++ b/public/locales/jp/translation.json @@ -140,7 +140,9 @@ "noProject": "プロジェクトなし", "unknownProject": "不明なプロジェクト", "tasks": "タスク", - "showingItems": "{{total}}件中{{current}}件を表示" + "showingItems": "{{total}}件中{{current}}件を表示", + "overdue": "期限切れ", + "planned": "計画中" }, "timeline": { "activityTimeline": "アクティビティタイムライン", @@ -580,7 +582,9 @@ "planned_desc": "スコープが決まり、開始準備が整った", "in_progress_desc": "アクティブな作業が行われている", "blocked_desc": "一時的に停止または行き詰まっている", - "completed_desc": "完了し、終了した" + "completed_desc": "完了し、終了した", + "active": "進行中", + "active_desc": "アクティブな作業が行われています" }, "showMetrics": "メトリクスを表示", "hideMetrics": "メトリクスを非表示" diff --git a/public/locales/ko/translation.json b/public/locales/ko/translation.json index d7070f1..746594e 100644 --- a/public/locales/ko/translation.json +++ b/public/locales/ko/translation.json @@ -140,7 +140,9 @@ "noProject": "프로젝트 없음", "unknownProject": "알 수 없는 프로젝트", "tasks": "작업", - "showingItems": "{{total}}개 중 {{current}}개 표시" + "showingItems": "{{total}}개 중 {{current}}개 표시", + "overdue": "연체", + "planned": "계획됨" }, "timeline": { "activityTimeline": "활동 타임라인", @@ -782,7 +784,9 @@ "planned_desc": "범위가 정의되고 시작할 준비가 됨", "in_progress_desc": "활동적인 작업 진행 중", "blocked_desc": "일시적으로 중단되거나 막힘", - "completed_desc": "완료되고 끝남" + "completed_desc": "완료되고 끝남", + "active": "진행 중", + "active_desc": "활동 중인 작업" }, "showMetrics": "메트릭 표시", "hideMetrics": "메트릭 숨기기" diff --git a/public/locales/nl/translation.json b/public/locales/nl/translation.json index 2d6d4f7..48ce981 100644 --- a/public/locales/nl/translation.json +++ b/public/locales/nl/translation.json @@ -140,7 +140,9 @@ "noProject": "Geen project", "unknownProject": "Onbekend project", "tasks": "taken", - "showingItems": "{{current}} van {{total}} items weergegeven" + "showingItems": "{{current}} van {{total}} items weergegeven", + "overdue": "Te laat", + "planned": "Gepland" }, "timeline": { "activityTimeline": "Activiteit Tijdlijn", @@ -782,7 +784,9 @@ "planned_desc": "Afgebakend en klaar om te starten", "in_progress_desc": "Actief werk aan de gang", "blocked_desc": "Tijdelijk gepauzeerd of vastgelopen", - "completed_desc": "Afgerond en gedaan" + "completed_desc": "Afgerond en gedaan", + "active": "In uitvoering", + "active_desc": "Actief werk aan de gang" }, "showMetrics": "Toon statistieken", "hideMetrics": "Verberg statistieken" diff --git a/public/locales/no/translation.json b/public/locales/no/translation.json index 9724e21..0062528 100644 --- a/public/locales/no/translation.json +++ b/public/locales/no/translation.json @@ -140,7 +140,9 @@ "noProject": "Ingen prosjekt", "unknownProject": "Ukjent prosjekt", "tasks": "oppgaver", - "showingItems": "Viser {{current}} av {{total}} elementer" + "showingItems": "Viser {{current}} av {{total}} elementer", + "overdue": "Forsinket", + "planned": "Planlagt" }, "timeline": { "activityTimeline": "Aktivitetslinje", @@ -782,7 +784,9 @@ "planned_desc": "Avgrenset og klar til å starte", "in_progress_desc": "Aktivt arbeid pågår", "blocked_desc": "Midlertidig pauset eller fastlåst", - "completed_desc": "Ferdig og gjort" + "completed_desc": "Ferdig og gjort", + "active": "Pågår", + "active_desc": "Aktivt arbeid pågår" }, "showMetrics": "Vis målinger", "hideMetrics": "Skjul målinger" diff --git a/public/locales/pl/translation.json b/public/locales/pl/translation.json index 9eaf83e..2b157c6 100644 --- a/public/locales/pl/translation.json +++ b/public/locales/pl/translation.json @@ -140,7 +140,9 @@ "noProject": "Brak projektu", "unknownProject": "Nieznany projekt", "tasks": "zadania", - "showingItems": "Wyświetlanie {{current}} z {{total}} elementów" + "showingItems": "Wyświetlanie {{current}} z {{total}} elementów", + "overdue": "Przeterminowane", + "planned": "Zaplanowane" }, "timeline": { "activityTimeline": "Oś czasu aktywności", @@ -782,7 +784,9 @@ "planned_desc": "Określone i gotowe do rozpoczęcia", "in_progress_desc": "Aktywna praca w toku", "blocked_desc": "Tymczasowo wstrzymane lub utknęło", - "completed_desc": "Zakończone i gotowe" + "completed_desc": "Zakończone i gotowe", + "active": "W trakcie", + "active_desc": "Aktywna praca w toku" }, "showMetrics": "Pokaż metryki", "hideMetrics": "Ukryj metryki" diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index 9dcf5d0..6412c34 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -140,7 +140,9 @@ "noProject": "Sem projeto", "unknownProject": "Projeto desconhecido", "tasks": "tarefas", - "showingItems": "Mostrando {{current}} de {{total}} itens" + "showingItems": "Mostrando {{current}} de {{total}} itens", + "overdue": "Atrasado", + "planned": "Planejado" }, "timeline": { "activityTimeline": "Linha do Tempo de Atividades", @@ -782,7 +784,9 @@ "planned_desc": "Escopado e pronto para começar", "in_progress_desc": "Trabalho ativo em andamento", "blocked_desc": "Pausado temporariamente ou preso", - "completed_desc": "Finalizado e concluído" + "completed_desc": "Finalizado e concluído", + "active": "Em Andamento", + "active_desc": "Trabalho ativo em andamento" }, "showMetrics": "Mostrar métricas", "hideMetrics": "Ocultar métricas" diff --git a/public/locales/ro/translation.json b/public/locales/ro/translation.json index 96a6326..022cd03 100644 --- a/public/locales/ro/translation.json +++ b/public/locales/ro/translation.json @@ -140,7 +140,9 @@ "noProject": "Fără proiect", "unknownProject": "Proiect necunoscut", "tasks": "sarcini", - "showingItems": "Se afișează {{current}} din {{total}} elemente" + "showingItems": "Se afișează {{current}} din {{total}} elemente", + "overdue": "Întârziat", + "planned": "Planificat" }, "timeline": { "activityTimeline": "Cronologia activităților", @@ -782,7 +784,9 @@ "planned_desc": "Definit și gata de început", "in_progress_desc": "Lucru activ în desfășurare", "blocked_desc": "Pauză temporară sau blocat", - "completed_desc": "Finalizat și terminat" + "completed_desc": "Finalizat și terminat", + "active": "În desfășurare", + "active_desc": "Lucrări active în desfășurare" }, "showMetrics": "Arată metrici", "hideMetrics": "Ascunde metrici" diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 2317be7..fc78022 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -140,7 +140,9 @@ "noProject": "Без проекта", "unknownProject": "Неизвестный проект", "tasks": "задачи", - "showingItems": "Показано {{current}} из {{total}} элементов" + "showingItems": "Показано {{current}} из {{total}} элементов", + "overdue": "Просрочено", + "planned": "Запланировано" }, "timeline": { "activityTimeline": "Хронология активности", @@ -782,7 +784,9 @@ "planned_desc": "Определено и готово к началу", "in_progress_desc": "Активная работа идет", "blocked_desc": "Временно приостановлено или застряло", - "completed_desc": "Завершено и выполнено" + "completed_desc": "Завершено и выполнено", + "active": "В процессе", + "active_desc": "Активная работа ведется" }, "showMetrics": "Показать метрики", "hideMetrics": "Скрыть метрики" diff --git a/public/locales/sl/translation.json b/public/locales/sl/translation.json index 39d20dd..f28d789 100644 --- a/public/locales/sl/translation.json +++ b/public/locales/sl/translation.json @@ -140,7 +140,9 @@ "noProject": "Brez projekta", "unknownProject": "Neznan projekt", "tasks": "naloge", - "showingItems": "Prikazovanje {{current}} od {{total}} elementov" + "showingItems": "Prikazovanje {{current}} od {{total}} elementov", + "overdue": "Zapadlo", + "planned": "Načrtovano" }, "timeline": { "activityTimeline": "Časovnica aktivnosti", @@ -782,7 +784,9 @@ "planned_desc": "Določeno in pripravljeno za začetek", "in_progress_desc": "Aktivno delo poteka", "blocked_desc": "Začasno ustavljeno ali zastojev", - "completed_desc": "Dokončano in opravljeno" + "completed_desc": "Dokončano in opravljeno", + "active": "V teku", + "active_desc": "Aktivno delo poteka" }, "showMetrics": "Prikaži metrike", "hideMetrics": "Skrij metrike" diff --git a/public/locales/sv/translation.json b/public/locales/sv/translation.json index 3348664..25b1dfb 100644 --- a/public/locales/sv/translation.json +++ b/public/locales/sv/translation.json @@ -140,7 +140,9 @@ "noProject": "Inget projekt", "unknownProject": "Okänt projekt", "tasks": "uppgifter", - "showingItems": "Visar {{current}} av {{total}} objekt" + "showingItems": "Visar {{current}} av {{total}} objekt", + "overdue": "Förfallen", + "planned": "Planerad" }, "timeline": { "activityTimeline": "Aktivitetslinje", @@ -782,7 +784,9 @@ "planned_desc": "Avgränsad och redo att starta", "in_progress_desc": "Aktivt arbete pågår", "blocked_desc": "Tillfälligt pausad eller fast", - "completed_desc": "Avslutad och klar" + "completed_desc": "Avslutad och klar", + "active": "Pågående", + "active_desc": "Aktivt arbete pågår" }, "showMetrics": "Visa mätvärden", "hideMetrics": "Dölj mätvärden" diff --git a/public/locales/tr/translation.json b/public/locales/tr/translation.json index 9b18b76..2ecaf44 100644 --- a/public/locales/tr/translation.json +++ b/public/locales/tr/translation.json @@ -140,7 +140,9 @@ "noProject": "Proje yok", "unknownProject": "Bilinmeyen proje", "tasks": "görevler", - "showingItems": "{{total}} öğeden {{current}} tanesi gösteriliyor" + "showingItems": "{{total}} öğeden {{current}} tanesi gösteriliyor", + "overdue": "Gecikmiş", + "planned": "Planlanmış" }, "timeline": { "activityTimeline": "Etkinlik Zaman Çizelgesi", @@ -782,7 +784,9 @@ "planned_desc": "Kapsam belirlendi ve başlamaya hazır", "in_progress_desc": "Aktif çalışma devam ediyor", "blocked_desc": "Geçici olarak duraklatıldı veya takıldı", - "completed_desc": "Tamamlandı ve sona erdi" + "completed_desc": "Tamamlandı ve sona erdi", + "active": "Devam Ediyor", + "active_desc": "Aktif çalışma sürüyor" }, "showMetrics": "Metrikleri göster", "hideMetrics": "Metrikleri gizle" diff --git a/public/locales/ua/translation.json b/public/locales/ua/translation.json index 2006e1d..5b4623a 100644 --- a/public/locales/ua/translation.json +++ b/public/locales/ua/translation.json @@ -140,7 +140,9 @@ "noProject": "Без проекту", "unknownProject": "Невідомий проект", "tasks": "завдання", - "showingItems": "Показано {{current}} з {{total}} елементів" + "showingItems": "Показано {{current}} з {{total}} елементів", + "overdue": "Прострочено", + "planned": "Заплановано" }, "timeline": { "activityTimeline": "Хронологія Активності", @@ -210,7 +212,9 @@ "planned_desc": "Визначено та готово до початку", "in_progress_desc": "Активна робота триває", "blocked_desc": "Тимчасово призупинено або застрягло", - "completed_desc": "Завершено та виконано" + "completed_desc": "Завершено та виконано", + "active": "В процесі", + "active_desc": "Активна робота триває" }, "showMetrics": "Показати метрики", "hideMetrics": "Сховати метрики" diff --git a/public/locales/vi/translation.json b/public/locales/vi/translation.json index 14fb5ee..a5e435d 100644 --- a/public/locales/vi/translation.json +++ b/public/locales/vi/translation.json @@ -140,7 +140,9 @@ "noProject": "Không có dự án", "unknownProject": "Dự án không xác định", "tasks": "nhiệm vụ", - "showingItems": "Hiển thị {{current}} trong số {{total}} mục" + "showingItems": "Hiển thị {{current}} trong số {{total}} mục", + "overdue": "Quá hạn", + "planned": "Đã lên kế hoạch" }, "timeline": { "activityTimeline": "Dòng Thời Gian Hoạt Động", @@ -782,7 +784,9 @@ "planned_desc": "Đã xác định và sẵn sàng bắt đầu", "in_progress_desc": "Công việc đang diễn ra", "blocked_desc": "Tạm dừng hoặc bị mắc kẹt", - "completed_desc": "Đã hoàn tất và xong" + "completed_desc": "Đã hoàn tất và xong", + "active": "Đang tiến hành", + "active_desc": "Công việc đang diễn ra" }, "showMetrics": "Hiển thị số liệu", "hideMetrics": "Ẩn số liệu" diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index 574a2a6..1b3bd90 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -140,7 +140,9 @@ "noProject": "无项目", "unknownProject": "未知项目", "tasks": "任务", - "showingItems": "显示 {{current}} / {{total}} 项" + "showingItems": "显示 {{current}} / {{total}} 项", + "overdue": "逾期", + "planned": "计划中" }, "timeline": { "activityTimeline": "活动时间线", @@ -782,7 +784,9 @@ "planned_desc": "已确定范围并准备开始", "in_progress_desc": "正在进行的工作", "blocked_desc": "暂时暂停或卡住", - "completed_desc": "已完成并结束" + "completed_desc": "已完成并结束", + "active": "进行中", + "active_desc": "正在进行的工作" }, "showMetrics": "显示指标", "hideMetrics": "隐藏指标"