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
This commit is contained in:
Chris 2025-12-02 18:00:36 +02:00 committed by GitHub
parent f663ad5d52
commit 2d2a989a5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1041 additions and 357 deletions

0
backend/database.sqlite Normal file
View file

View file

@ -57,18 +57,27 @@ async function addDashboardLists(
const listKeys = [ const listKeys = [
'tasks_in_progress', 'tasks_in_progress',
'tasks_today_plan',
'tasks_due_today', 'tasks_due_today',
'tasks_overdue',
'suggested_tasks', 'suggested_tasks',
'tasks_completed_today', 'tasks_completed_today',
]; ];
const serializedLists = {};
for (const key of listKeys) { for (const key of listKeys) {
response[key] = await serializeTasks( const metricsKey =
metricsData[key], key === 'tasks_today_plan' ? 'today_plan_tasks' : key;
serializedLists[key] = await serializeTasks(
metricsData[metricsKey],
timezone, timezone,
serializationOptions serializationOptions
); );
} }
Object.assign(response, serializedLists);
response.dashboard_lists = serializedLists;
} }
function addPerformanceHeaders(res, startTime, queryStats) { function addPerformanceHeaders(res, startTime, queryStats) {

View file

@ -8,6 +8,7 @@ const {
fetchTasksInProgress, fetchTasksInProgress,
fetchTodayPlanTasks, fetchTodayPlanTasks,
fetchTasksDueToday, fetchTasksDueToday,
fetchOverdueTasks,
fetchSomedayTaskIds, fetchSomedayTaskIds,
fetchNonProjectTasks, fetchNonProjectTasks,
fetchProjectTasks, fetchProjectTasks,
@ -140,6 +141,7 @@ async function computeTaskMetrics(
tasksInProgress, tasksInProgress,
todayPlanTasks, todayPlanTasks,
tasksDueToday, tasksDueToday,
tasksOverdue,
tasksCompletedToday, tasksCompletedToday,
weeklyCompletions, weeklyCompletions,
] = await Promise.all([ ] = await Promise.all([
@ -148,6 +150,7 @@ async function computeTaskMetrics(
fetchTasksInProgress(visibleTasksWhere), fetchTasksInProgress(visibleTasksWhere),
fetchTodayPlanTasks(visibleTasksWhere), fetchTodayPlanTasks(visibleTasksWhere),
fetchTasksDueToday(visibleTasksWhere, userTimezone), fetchTasksDueToday(visibleTasksWhere, userTimezone),
fetchOverdueTasks(visibleTasksWhere, userTimezone),
fetchTasksCompletedToday(userId, userTimezone), fetchTasksCompletedToday(userId, userTimezone),
computeWeeklyCompletions(userId, userTimezone), computeWeeklyCompletions(userId, userTimezone),
]); ]);
@ -167,6 +170,7 @@ async function computeTaskMetrics(
tasks_in_progress_count: tasksInProgress.length, tasks_in_progress_count: tasksInProgress.length,
tasks_in_progress: tasksInProgress, tasks_in_progress: tasksInProgress,
tasks_due_today: tasksDueToday, tasks_due_today: tasksDueToday,
tasks_overdue: tasksOverdue,
today_plan_tasks: todayPlanTasks, today_plan_tasks: todayPlanTasks,
suggested_tasks: suggestedTasks, suggested_tasks: suggestedTasks,
tasks_completed_today: tasksCompletedToday, tasks_completed_today: tasksCompletedToday,
@ -176,8 +180,41 @@ async function computeTaskMetrics(
async function getTaskMetrics(userId, timezone) { async function getTaskMetrics(userId, timezone) {
const metrics = await computeTaskMetrics(userId, timezone); const metrics = await computeTaskMetrics(userId, timezone);
const { buildMetricsResponse } = require('../core/serializers'); const {
return await buildMetricsResponse(metrics); 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 = { module.exports = {

View file

@ -47,18 +47,22 @@ async function fetchTasksInProgress(visibleTasksWhere) {
async function fetchTodayPlanTasks(visibleTasksWhere) { async function fetchTodayPlanTasks(visibleTasksWhere) {
return await Task.findAll({ return await Task.findAll({
where: { where: {
...visibleTasksWhere, [Op.and]: [
today: true, visibleTasksWhere,
status: { {
[Op.notIn]: [ today: true,
Task.STATUS.DONE, status: {
Task.STATUS.ARCHIVED, [Op.notIn]: [
'done', Task.STATUS.DONE,
'archived', Task.STATUS.ARCHIVED,
], 'done',
}, 'archived',
parent_task_id: null, ],
recurring_parent_id: null, },
parent_task_id: null,
recurring_parent_id: null,
},
],
}, },
include: getTaskIncludeConfig(), include: getTaskIncludeConfig(),
order: [ order: [
@ -87,11 +91,20 @@ async function fetchTasksDueToday(visibleTasksWhere, userTimezone) {
}, },
parent_task_id: null, parent_task_id: null,
recurring_parent_id: null, recurring_parent_id: null,
today: { [Op.or]: [false, null] },
[Op.or]: [ [Op.or]: [
{ due_date: { [Op.lte]: todayBounds.end } }, {
due_date: {
[Op.and]: [
{ [Op.gte]: todayBounds.start },
{ [Op.lte]: todayBounds.end },
],
},
},
sequelize.literal(`EXISTS ( sequelize.literal(`EXISTS (
SELECT 1 FROM projects SELECT 1 FROM projects
WHERE projects.id = Task.project_id WHERE projects.id = Task.project_id
AND projects.due_date_at >= '${todayBounds.start.toISOString()}'
AND projects.due_date_at <= '${todayBounds.end.toISOString()}' AND projects.due_date_at <= '${todayBounds.end.toISOString()}'
)`), )`),
], ],
@ -99,6 +112,51 @@ async function fetchTasksDueToday(visibleTasksWhere, userTimezone) {
], ],
}, },
include: getTaskIncludeConfig(), 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(), include: getTaskIncludeConfig(),
order: [ order: [
['priority', 'DESC'], ['priority', 'DESC'],
['created_at', 'ASC'], ['due_date', 'ASC'],
['project_id', 'ASC'],
], ],
limit: 6, limit: 6,
}); });
@ -160,7 +219,8 @@ async function fetchProjectTasks(
include: getTaskIncludeConfig(), include: getTaskIncludeConfig(),
order: [ order: [
['priority', 'DESC'], ['priority', 'DESC'],
['created_at', 'ASC'], ['due_date', 'ASC'],
['project_id', 'ASC'],
], ],
limit: 6, limit: 6,
}); });
@ -188,16 +248,16 @@ async function fetchSomedayFallbackTasks(
include: getTaskIncludeConfig(), include: getTaskIncludeConfig(),
order: [ order: [
['priority', 'DESC'], ['priority', 'DESC'],
['created_at', 'ASC'], ['due_date', 'ASC'],
['project_id', 'ASC'],
], ],
limit: limit, limit: limit,
}); });
} }
async function fetchTasksCompletedToday(userId, userTimezone) { async function fetchTasksCompletedToday(userId, userTimezone) {
const todayInUserTz = moment.tz(userTimezone); const safeTimezone = getSafeTimezone(userTimezone);
const todayStart = todayInUserTz.clone().startOf('day').utc().toDate(); const todayBounds = getTodayBoundsInUTC(safeTimezone);
const todayEnd = todayInUserTz.clone().endOf('day').utc().toDate();
return await Task.findAll({ return await Task.findAll({
where: { where: {
@ -206,7 +266,8 @@ async function fetchTasksCompletedToday(userId, userTimezone) {
parent_task_id: null, parent_task_id: null,
recurring_parent_id: null, recurring_parent_id: null,
completed_at: { completed_at: {
[Op.between]: [todayStart, todayEnd], [Op.gte]: todayBounds.start,
[Op.lte]: todayBounds.end,
}, },
}, },
include: getTaskIncludeConfig(), include: getTaskIncludeConfig(),
@ -220,6 +281,7 @@ module.exports = {
fetchTasksInProgress, fetchTasksInProgress,
fetchTodayPlanTasks, fetchTodayPlanTasks,
fetchTasksDueToday, fetchTasksDueToday,
fetchOverdueTasks,
fetchSomedayTaskIds, fetchSomedayTaskIds,
fetchNonProjectTasks, fetchNonProjectTasks,
fetchProjectTasks, fetchProjectTasks,

View file

@ -131,7 +131,6 @@ async function filterTasksByParams(
{ recurrence_type: { [Op.ne]: 'none' } }, { recurrence_type: { [Op.ne]: 'none' } },
{ recurrence_type: { [Op.ne]: null } }, { recurrence_type: { [Op.ne]: null } },
{ recurring_parent_id: null }, { recurring_parent_id: null },
{ today: true },
], ],
}, },
{ {

View file

@ -225,16 +225,22 @@ describe('Timezone Fixes Integration Tests', () => {
expect(tasksRes.statusCode).toBe(200); 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( 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 (task) => task.name
); );
expect(taskNames).toContain('Today Task'); expect(dueTodayNames).toContain('Today Task');
expect(taskNames).toContain('Yesterday Task'); expect(tasksRes.body.tasks_overdue.length).toBeGreaterThanOrEqual(
1
);
const overdueNames = tasksRes.body.tasks_overdue.map(
(task) => task.name
);
expect(overdueNames).toContain('Yesterday Task');
}); });
}); });

View file

@ -12,26 +12,17 @@ red() { printf "\033[31m%s\033[0m\n" "$*"; }
green() { printf "\033[32m%s\033[0m\n" "$*"; } green() { printf "\033[32m%s\033[0m\n" "$*"; }
yellow() { printf "\033[33m%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)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
E2E_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" E2E_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ROOT_DIR="$(cd "$E2E_DIR/.." && pwd)" ROOT_DIR="$(cd "$E2E_DIR/.." && pwd)"
cd "$E2E_DIR" cd "$ROOT_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
# Check if Playwright is installed
if ! npx playwright --version >/dev/null 2>&1; then if ! npx playwright --version >/dev/null 2>&1; then
yellow "Installing Playwright browsers..." yellow "Installing Playwright browsers..."
npm run install-browsers npx playwright install --with-deps
fi fi
# Start backend and frontend # Start backend and frontend
@ -108,8 +99,8 @@ for i in {1..60}; do
fi fi
done done
# Run tests # Run tests (specify config file since we're running from root)
cd "$E2E_DIR" cd "$ROOT_DIR"
yellow "Running Playwright tests..." yellow "Running Playwright tests..."
APP_URL="$FRONTEND_URL" \ APP_URL="$FRONTEND_URL" \
@ -117,11 +108,11 @@ E2E_EMAIL="${E2E_EMAIL:-test@tududi.com}" \
E2E_PASSWORD="${E2E_PASSWORD:-password123}" \ E2E_PASSWORD="${E2E_PASSWORD:-password123}" \
bash -c ' bash -c '
if [ "${E2E_MODE:-}" = "ui" ]; then if [ "${E2E_MODE:-}" = "ui" ]; then
npm run test:ui npx playwright test --ui --config=e2e/playwright.config.ts
elif [ "${E2E_MODE:-}" = "headed" ]; then elif [ "${E2E_MODE:-}" = "headed" ]; then
# Respect E2E_SLOWMO and run only Firefox sequentially # Respect E2E_SLOWMO and run only Chromium sequentially
npx playwright test --headed --project=Firefox --workers=1 npx playwright test --headed --project=Chromium --workers=1 --config=e2e/playwright.config.ts
else else
npx playwright test --workers=5 npx playwright test --config=e2e/playwright.config.ts
fi fi
' '

92
e2e/package-lock.json generated
View file

@ -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"
}
}
}
}

View file

@ -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"
}
}

View file

@ -11,6 +11,7 @@ export default defineConfig({
timeout: 60_000, timeout: 60_000,
expect: { timeout: 10_000 }, expect: { timeout: 10_000 },
fullyParallel: true, fullyParallel: true,
workers: process.env.CI ? 1 : undefined, // Use default workers locally, 1 in CI
reporter: [['list']], reporter: [['list']],
use: { use: {
baseURL, baseURL,

View file

@ -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
}
});
});

View file

@ -93,6 +93,18 @@ const TasksToday: React.FC = () => {
const stored = localStorage.getItem('completedTasksCollapsed'); const stored = localStorage.getItem('completedTasksCollapsed');
return stored === 'true'; 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); const [isSettingsLoaded, setIsSettingsLoaded] = useState(false);
// Metrics from the API (counts) + task arrays stored locally // Metrics from the API (counts) + task arrays stored locally
@ -100,6 +112,7 @@ const TasksToday: React.FC = () => {
Metrics & { Metrics & {
tasks_in_progress?: Task[]; tasks_in_progress?: Task[];
tasks_due_today?: Task[]; tasks_due_today?: Task[];
tasks_overdue?: Task[];
today_plan_tasks?: Task[]; today_plan_tasks?: Task[];
suggested_tasks?: Task[]; suggested_tasks?: Task[];
tasks_completed_today?: Task[]; tasks_completed_today?: Task[];
@ -116,6 +129,7 @@ const TasksToday: React.FC = () => {
// Task arrays (fetched separately via include_lists parameter) // Task arrays (fetched separately via include_lists parameter)
tasks_in_progress: [], tasks_in_progress: [],
tasks_due_today: [], tasks_due_today: [],
tasks_overdue: [],
today_plan_tasks: [], today_plan_tasks: [],
suggested_tasks: [], suggested_tasks: [],
tasks_completed_today: [], tasks_completed_today: [],
@ -132,6 +146,9 @@ const TasksToday: React.FC = () => {
// Client-side pagination for Due Today tasks (since backend returns all) // Client-side pagination for Due Today tasks (since backend returns all)
const [dueTodayDisplayLimit, setDueTodayDisplayLimit] = useState(20); 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) // Client-side pagination for Completed Today tasks (since backend returns all)
const [completedTodayDisplayLimit, setCompletedTodayDisplayLimit] = const [completedTodayDisplayLimit, setCompletedTodayDisplayLimit] =
useState(20); useState(20);
@ -220,6 +237,24 @@ const TasksToday: React.FC = () => {
localStorage.setItem('completedTasksCollapsed', newState.toString()); 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 // Load data once on component mount
useEffect(() => { useEffect(() => {
isMounted.current = true; isMounted.current = true;
@ -241,6 +276,7 @@ const TasksToday: React.FC = () => {
// Store task arrays locally (fetched via include_lists=true) // Store task arrays locally (fetched via include_lists=true)
tasks_in_progress: result.tasks_in_progress || [], tasks_in_progress: result.tasks_in_progress || [],
tasks_due_today: result.tasks_due_today || [], tasks_due_today: result.tasks_due_today || [],
tasks_overdue: result.tasks_overdue || [],
today_plan_tasks: result.tasks || [], // Main tasks array is today plan today_plan_tasks: result.tasks || [], // Main tasks array is today plan
suggested_tasks: result.suggested_tasks || [], suggested_tasks: result.suggested_tasks || [],
tasks_completed_today: tasks_completed_today:
@ -511,6 +547,9 @@ const TasksToday: React.FC = () => {
newMetrics.tasks_due_today = removeTask( newMetrics.tasks_due_today = removeTask(
newMetrics.tasks_due_today || [] newMetrics.tasks_due_today || []
); );
newMetrics.tasks_overdue = removeTask(
newMetrics.tasks_overdue || []
);
newMetrics.tasks_in_progress = removeTask( newMetrics.tasks_in_progress = removeTask(
newMetrics.tasks_in_progress || [] newMetrics.tasks_in_progress || []
); );
@ -553,13 +592,9 @@ const TasksToday: React.FC = () => {
updatedTask updatedTask
); );
} }
// Check if due today (and not already in today_plan_tasks or in_progress) // Check if task has a due date (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');
if ( if (
isDueToday && updatedTask.due_date &&
updatedTask.status !== 'archived' && updatedTask.status !== 'archived' &&
!newMetrics.today_plan_tasks.some( !newMetrics.today_plan_tasks.some(
(t) => t.id === updatedTask.id (t) => t.id === updatedTask.id
@ -568,10 +603,26 @@ const TasksToday: React.FC = () => {
(t) => t.id === updatedTask.id (t) => t.id === updatedTask.id
) )
) { ) {
newMetrics.tasks_due_today = updateOrAddTask( const today = new Date();
newMetrics.tasks_due_today, const todayStr = format(today, 'yyyy-MM-dd');
updatedTask 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) // Check for suggested tasks (and not already in other active lists)
const isSuggested = const isSuggested =
@ -611,6 +662,7 @@ const TasksToday: React.FC = () => {
newMetrics.today_plan_tasks.length + newMetrics.today_plan_tasks.length +
newMetrics.suggested_tasks.length + newMetrics.suggested_tasks.length +
newMetrics.tasks_due_today.length + newMetrics.tasks_due_today.length +
newMetrics.tasks_overdue.length +
newMetrics.tasks_in_progress.length; newMetrics.tasks_in_progress.length;
return newMetrics; return newMetrics;
@ -670,6 +722,11 @@ const TasksToday: React.FC = () => {
newMetrics.tasks_due_today newMetrics.tasks_due_today
); );
} }
if (newMetrics.tasks_overdue) {
newMetrics.tasks_overdue = updateTaskInList(
newMetrics.tasks_overdue
);
}
if (newMetrics.tasks_in_progress) { if (newMetrics.tasks_in_progress) {
newMetrics.tasks_in_progress = updateTaskInList( newMetrics.tasks_in_progress = updateTaskInList(
newMetrics.tasks_in_progress newMetrics.tasks_in_progress
@ -712,6 +769,7 @@ const TasksToday: React.FC = () => {
...result.metrics, ...result.metrics,
tasks_in_progress: result.tasks_in_progress || [], tasks_in_progress: result.tasks_in_progress || [],
tasks_due_today: result.tasks_due_today || [], tasks_due_today: result.tasks_due_today || [],
tasks_overdue: result.tasks_overdue || [],
today_plan_tasks: result.tasks || [], today_plan_tasks: result.tasks || [],
suggested_tasks: result.suggested_tasks || [], suggested_tasks: result.suggested_tasks || [],
tasks_completed_today: tasks_completed_today:
@ -740,6 +798,7 @@ const TasksToday: React.FC = () => {
...result.metrics, ...result.metrics,
tasks_in_progress: result.tasks_in_progress || [], tasks_in_progress: result.tasks_in_progress || [],
tasks_due_today: result.tasks_due_today || [], tasks_due_today: result.tasks_due_today || [],
tasks_overdue: result.tasks_overdue || [],
today_plan_tasks: result.tasks || [], today_plan_tasks: result.tasks || [],
suggested_tasks: result.suggested_tasks || [], suggested_tasks: result.suggested_tasks || [],
tasks_completed_today: tasks_completed_today:
@ -798,6 +857,7 @@ const TasksToday: React.FC = () => {
...result.metrics, ...result.metrics,
tasks_in_progress: result.tasks_in_progress || [], tasks_in_progress: result.tasks_in_progress || [],
tasks_due_today: result.tasks_due_today || [], tasks_due_today: result.tasks_due_today || [],
tasks_overdue: result.tasks_overdue || [],
today_plan_tasks: result.tasks || [], today_plan_tasks: result.tasks || [],
suggested_tasks: result.suggested_tasks || [], suggested_tasks: result.suggested_tasks || [],
tasks_completed_today: tasks_completed_today:
@ -817,6 +877,10 @@ const TasksToday: React.FC = () => {
...(prevMetrics.tasks_due_today || []), ...(prevMetrics.tasks_due_today || []),
...(result.tasks_due_today || []), ...(result.tasks_due_today || []),
], ],
tasks_overdue: [
...(prevMetrics.tasks_overdue || []),
...(result.tasks_overdue || []),
],
today_plan_tasks: [ today_plan_tasks: [
...(prevMetrics.today_plan_tasks || []), ...(prevMetrics.today_plan_tasks || []),
...(result.tasks || []), ...(result.tasks || []),
@ -1200,80 +1264,309 @@ const TasksToday: React.FC = () => {
</div> </div>
) : null} ) : null}
{/* Today Plan */} {/* Overdue Tasks - Displayed first */}
<TodayPlan {isSettingsLoaded &&
todayPlanTasks={metrics.today_plan_tasks || []} todaySettings.showDueToday &&
projects={localProjects} metrics.tasks_overdue.length > 0 && (
onTaskUpdate={handleTaskUpdate} <div className="mb-6" data-testid="overdue-section">
onTaskDelete={handleTaskDelete} <div
onToggleToday={handleToggleToday} className="flex items-center justify-between cursor-pointer mt-6 mb-2 pb-2 border-b border-gray-200 dark:border-gray-700"
onTaskCompletionToggle={handleTaskCompletionToggle} onClick={toggleOverdueCollapsed}
/> data-testid="overdue-section-header"
>
<h3 className="text-xl font-medium text-red-600 dark:text-red-400">
{t('tasks.overdue', 'Overdue')}
</h3>
<div className="flex items-center">
<span className="text-sm text-gray-500 mr-2">
{metrics.tasks_overdue.length}
</span>
{isOverdueCollapsed ? (
<ChevronRightIcon className="h-5 w-5 text-gray-500" />
) : (
<ChevronDownIcon className="h-5 w-5 text-gray-500" />
)}
</div>
</div>
{!isOverdueCollapsed && (
<>
<TaskList
tasks={metrics.tasks_overdue.slice(
0,
overdueDisplayLimit
)}
onTaskUpdate={handleTaskUpdate}
onTaskDelete={handleTaskDelete}
projects={localProjects}
onToggleToday={handleToggleToday}
onTaskCompletionToggle={
handleTaskCompletionToggle
}
/>
{/* Load More Buttons for Today Plan Tasks */} {/* Load More Buttons for Overdue Tasks */}
{pagination.hasMore && ( {overdueDisplayLimit <
<div className="flex justify-center pt-4 pb-2 gap-3"> metrics.tasks_overdue.length && (
<button <div className="flex justify-center pt-4 pb-2 gap-3">
onClick={() => handleLoadMore(false)} <button
disabled={isLoading} onClick={() =>
className="inline-flex items-center px-5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" setOverdueDisplayLimit(
> (prev) => prev + 20
{isLoading ? ( )
<> }
<svg className="inline-flex items-center px-5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
className="animate-spin -ml-1 mr-2 h-4 w-4 text-gray-500" >
xmlns="http://www.w3.org/2000/svg" <QueueListIcon className="h-4 w-4 mr-2" />
fill="none" {t(
viewBox="0 0 24 24" 'common.loadMore',
> 'Load More'
<circle )}
className="opacity-25" </button>
cx="12" <button
cy="12" onClick={() =>
r="10" setOverdueDisplayLimit(
stroke="currentColor" metrics.tasks_overdue
strokeWidth="4" .length
></circle> )
<path }
className="opacity-75" className="inline-flex items-center px-5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
fill="currentColor" >
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" {t(
></path> 'common.showAll',
</svg> 'Show All'
{t('common.loading', 'Loading...')} )}
</> </button>
) : ( </div>
<> )}
<QueueListIcon className="h-4 w-4 mr-2" />
{t('common.loadMore', 'Load More')} {/* Pagination info for Overdue tasks */}
<div className="text-center text-sm text-gray-500 dark:text-gray-400 pt-2 pb-4">
{t(
'tasks.showingItems',
'Showing {{current}} of {{total}} items',
{
current: Math.min(
overdueDisplayLimit,
metrics.tasks_overdue.length
),
total: metrics.tasks_overdue
.length,
}
)}
</div>
</> </>
)} )}
</button> </div>
<button )}
onClick={() => handleLoadMore(true)}
disabled={isLoading}
className="inline-flex items-center px-5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{t('common.showAll', 'Show All')}
</button>
</div>
)}
{/* Pagination info for Today Plan tasks */} {/* Today Plan */}
{(metrics.today_plan_tasks || []).length > 0 && ( {(metrics.today_plan_tasks || []).length > 0 && (
<div className="text-center text-sm text-gray-500 dark:text-gray-400 pt-2 pb-4"> <div className="mb-6" data-testid="planned-section">
{t( <div
'tasks.showingItems', className="flex items-center justify-between cursor-pointer mt-6 mb-2 pb-2 border-b border-gray-200 dark:border-gray-700"
'Showing {{current}} of {{total}} items', onClick={toggleTodayPlanCollapsed}
{ data-testid="planned-section-header"
current: (metrics.today_plan_tasks || []) >
.length, <h3 className="text-xl font-medium">
total: pagination.total, {t('tasks.planned', 'Planned')}
} </h3>
<div className="flex items-center">
<span className="text-sm text-gray-500 mr-2">
{(metrics.today_plan_tasks || []).length}
</span>
{isTodayPlanCollapsed ? (
<ChevronRightIcon className="h-5 w-5 text-gray-500" />
) : (
<ChevronDownIcon className="h-5 w-5 text-gray-500" />
)}
</div>
</div>
{!isTodayPlanCollapsed && (
<>
<TodayPlan
todayPlanTasks={
metrics.today_plan_tasks || []
}
projects={localProjects}
onTaskUpdate={handleTaskUpdate}
onTaskDelete={handleTaskDelete}
onToggleToday={handleToggleToday}
onTaskCompletionToggle={
handleTaskCompletionToggle
}
/>
{/* Load More Buttons for Today Plan Tasks */}
{pagination.hasMore && (
<div className="flex justify-center pt-4 pb-2 gap-3">
<button
onClick={() =>
handleLoadMore(false)
}
disabled={isLoading}
className="inline-flex items-center px-5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isLoading ? (
<>
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{t(
'common.loading',
'Loading...'
)}
</>
) : (
<>
<QueueListIcon className="h-4 w-4 mr-2" />
{t(
'common.loadMore',
'Load More'
)}
</>
)}
</button>
<button
onClick={() => handleLoadMore(true)}
disabled={isLoading}
className="inline-flex items-center px-5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{t('common.showAll', 'Show All')}
</button>
</div>
)}
{/* Pagination info for Today Plan tasks */}
<div className="text-center text-sm text-gray-500 dark:text-gray-400 pt-2 pb-4">
{t(
'tasks.showingItems',
'Showing {{current}} of {{total}} items',
{
current: (
metrics.today_plan_tasks || []
).length,
total: pagination.total,
}
)}
</div>
</>
)} )}
</div> </div>
)} )}
{/* Due Today Tasks */}
{isSettingsLoaded &&
todaySettings.showDueToday &&
metrics.tasks_due_today.length > 0 && (
<div className="mb-6" data-testid="due-today-section">
<div
className="flex items-center justify-between cursor-pointer mt-6 mb-2 pb-2 border-b border-gray-200 dark:border-gray-700"
onClick={toggleDueTodayCollapsed}
data-testid="due-today-section-header"
>
<h3 className="text-xl font-medium">
{t('tasks.dueToday')}
</h3>
<div className="flex items-center">
<span className="text-sm text-gray-500 mr-2">
{metrics.tasks_due_today.length}
</span>
{isDueTodayCollapsed ? (
<ChevronRightIcon className="h-5 w-5 text-gray-500" />
) : (
<ChevronDownIcon className="h-5 w-5 text-gray-500" />
)}
</div>
</div>
{!isDueTodayCollapsed && (
<>
<TaskList
tasks={metrics.tasks_due_today.slice(
0,
dueTodayDisplayLimit
)}
onTaskUpdate={handleTaskUpdate}
onTaskDelete={handleTaskDelete}
projects={localProjects}
onToggleToday={handleToggleToday}
onTaskCompletionToggle={
handleTaskCompletionToggle
}
/>
{/* Load More Buttons for Due Today Tasks */}
{dueTodayDisplayLimit <
metrics.tasks_due_today.length && (
<div className="flex justify-center pt-4 pb-2 gap-3">
<button
onClick={() =>
setDueTodayDisplayLimit(
(prev) => prev + 20
)
}
className="inline-flex items-center px-5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
>
<QueueListIcon className="h-4 w-4 mr-2" />
{t(
'common.loadMore',
'Load More'
)}
</button>
<button
onClick={() =>
setDueTodayDisplayLimit(
metrics.tasks_due_today
.length
)
}
className="inline-flex items-center px-5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
>
{t(
'common.showAll',
'Show All'
)}
</button>
</div>
)}
{/* Pagination info for Due Today tasks */}
<div className="text-center text-sm text-gray-500 dark:text-gray-400 pt-2 pb-4">
{t(
'tasks.showingItems',
'Showing {{current}} of {{total}} items',
{
current: Math.min(
dueTodayDisplayLimit,
metrics.tasks_due_today
.length
),
total: metrics.tasks_due_today
.length,
}
)}
</div>
</>
)}
</div>
)}
{/* Suggested Tasks - Separate setting */} {/* Suggested Tasks - Separate setting */}
{!isSettingsLoaded ? ( {!isSettingsLoaded ? (
// Invisible placeholder for suggestions // Invisible placeholder for suggestions
@ -1319,83 +1612,20 @@ const TasksToday: React.FC = () => {
</div> </div>
) : null} ) : null}
{/* Due Today Tasks - Conditionally Rendered */}
{isSettingsLoaded &&
todaySettings.showDueToday &&
metrics.tasks_due_today.length > 0 && (
<div className="mb-6">
<h3 className="text-xl font-medium mt-6 mb-2">
{t('tasks.dueToday')}
</h3>
<TaskList
tasks={metrics.tasks_due_today.slice(
0,
dueTodayDisplayLimit
)}
onTaskUpdate={handleTaskUpdate}
onTaskDelete={handleTaskDelete}
projects={localProjects}
onToggleToday={handleToggleToday}
onTaskCompletionToggle={
handleTaskCompletionToggle
}
/>
{/* Load More Buttons for Due Today Tasks */}
{dueTodayDisplayLimit <
metrics.tasks_due_today.length && (
<div className="flex justify-center pt-4 pb-2 gap-3">
<button
onClick={() =>
setDueTodayDisplayLimit(
(prev) => prev + 20
)
}
className="inline-flex items-center px-5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
>
<QueueListIcon className="h-4 w-4 mr-2" />
{t('common.loadMore', 'Load More')}
</button>
<button
onClick={() =>
setDueTodayDisplayLimit(
metrics.tasks_due_today.length
)
}
className="inline-flex items-center px-5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
>
{t('common.showAll', 'Show All')}
</button>
</div>
)}
{/* Pagination info for Due Today tasks */}
<div className="text-center text-sm text-gray-500 dark:text-gray-400 pt-2 pb-4">
{t(
'tasks.showingItems',
'Showing {{current}} of {{total}} items',
{
current: Math.min(
dueTodayDisplayLimit,
metrics.tasks_due_today.length
),
total: metrics.tasks_due_today.length,
}
)}
</div>
</div>
)}
{/* Completed Tasks - Conditionally Rendered */} {/* Completed Tasks - Conditionally Rendered */}
{isSettingsLoaded && {isSettingsLoaded &&
todaySettings.showCompleted && todaySettings.showCompleted &&
(() => { (() => {
const completedToday = metrics.tasks_completed_today; // Use the already filtered list from backend const completedToday = metrics.tasks_completed_today; // Use the already filtered list from backend
return ( return (
<div className="mb-6"> <div
className="mb-6"
data-testid="completed-section"
>
<div <div
className="flex items-center justify-between cursor-pointer mt-6 mb-2 pb-2 border-b border-gray-200 dark:border-gray-700" className="flex items-center justify-between cursor-pointer mt-6 mb-2 pb-2 border-b border-gray-200 dark:border-gray-700"
onClick={toggleCompletedCollapsed} onClick={toggleCompletedCollapsed}
data-testid="completed-section-header"
> >
<h3 className="text-xl font-medium"> <h3 className="text-xl font-medium">
{t('tasks.completedToday')} {t('tasks.completedToday')}

View file

@ -35,17 +35,66 @@ const TodayPlan: React.FC<TodayPlanProps> = ({
const aInProgress = a.status === 'in_progress' || a.status === 1; const aInProgress = a.status === 'in_progress' || a.status === 1;
const bInProgress = b.status === 'in_progress' || b.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) { if (aInProgress && bInProgress) {
// Recently updated tasks should be at the bottom of in-progress group // 1. Priority (High → Medium → Low → None)
const aUpdated = new Date(a.updated_at || a.created_at || 0); const priorityOrder = { high: 3, medium: 2, low: 1 };
const bUpdated = new Date(b.updated_at || b.created_at || 0); const aPriority =
return aUpdated.getTime() - bUpdated.getTime(); // Older tasks first, newer to bottom 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) { 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 // Put in-progress tasks first

View file

@ -19,6 +19,7 @@ export const fetchTasks = async (
groupedTasks?: GroupedTasks; groupedTasks?: GroupedTasks;
tasks_in_progress?: Task[]; tasks_in_progress?: Task[];
tasks_due_today?: Task[]; tasks_due_today?: Task[];
tasks_overdue?: Task[];
suggested_tasks?: Task[]; suggested_tasks?: Task[];
tasks_completed_today?: Task[]; tasks_completed_today?: Task[];
pagination?: { pagination?: {
@ -64,6 +65,7 @@ export const fetchTasks = async (
// Dashboard task lists (only present when include_lists=true) // Dashboard task lists (only present when include_lists=true)
tasks_in_progress: tasksResult.tasks_in_progress, tasks_in_progress: tasksResult.tasks_in_progress,
tasks_due_today: tasksResult.tasks_due_today, tasks_due_today: tasksResult.tasks_due_today,
tasks_overdue: tasksResult.tasks_overdue,
suggested_tasks: tasksResult.suggested_tasks, suggested_tasks: tasksResult.suggested_tasks,
tasks_completed_today: tasksResult.tasks_completed_today, tasks_completed_today: tasksResult.tasks_completed_today,
// Pagination metadata // Pagination metadata

60
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "v0.87-beta.2", "version": "v0.87-beta.2",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@playwright/test": "^1.57.0",
"bcrypt": "~6.0.0", "bcrypt": "~6.0.0",
"compression": "~1.8.0", "compression": "~1.8.0",
"compromise": "^14.14.4", "compromise": "^14.14.4",
@ -3213,6 +3214,21 @@
"url": "https://opencollective.com/pkgr" "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": { "node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.17", "version": "0.5.17",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz", "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" "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": { "node_modules/possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",

View file

@ -14,6 +14,7 @@
"test": "npm run backend:test", "test": "npm run backend:test",
"test:backend": "npm run backend:test", "test:backend": "npm run backend:test",
"test:ui": "bash e2e/bin/run-e2e.sh && echo \"Success!\"", "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:ui:headed": "cross-env E2E_MODE=headed E2E_SLOWMO=500 bash e2e/bin/run-e2e.sh",
"test:watch": "npm run frontend:test:watch", "test:watch": "npm run frontend:test:watch",
"test:coverage": "npm run frontend:test:coverage && npm run backend:test:coverage", "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", "lint:fix": "npm run frontend:lint:fix && npm run backend:lint:fix",
"format": "npm run frontend:format && npm run backend:format", "format": "npm run frontend:format && npm run backend:format",
"format:fix": "npm run frontend:format:fix && npm run backend:format:fix", "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": [], "keywords": [],
"author": "", "author": "",
@ -132,6 +134,7 @@
"zustand": "^5.0.3" "zustand": "^5.0.3"
}, },
"dependencies": { "dependencies": {
"@playwright/test": "^1.57.0",
"bcrypt": "~6.0.0", "bcrypt": "~6.0.0",
"compression": "~1.8.0", "compression": "~1.8.0",
"compromise": "^14.14.4", "compromise": "^14.14.4",

View file

@ -103,7 +103,7 @@
"dueToday": "مستحقة اليوم", "dueToday": "مستحقة اليوم",
"stale": "قديمة", "stale": "قديمة",
"suggested": "مقترحة", "suggested": "مقترحة",
"completedToday": "مكتمل اليوم", "completedToday": "مكتمل",
"weeklyCompletions": "التقدم الأسبوعي", "weeklyCompletions": "التقدم الأسبوعي",
"taskCompleted": "تم إكمال المهمة", "taskCompleted": "تم إكمال المهمة",
"tasksCompleted": "تم إكمال المهام", "tasksCompleted": "تم إكمال المهام",
@ -140,7 +140,9 @@
"noProject": "بدون مشروع", "noProject": "بدون مشروع",
"unknownProject": "مشروع غير معروف", "unknownProject": "مشروع غير معروف",
"tasks": "مهام", "tasks": "مهام",
"showingItems": "عرض {{current}} من {{total}} عنصر" "showingItems": "عرض {{current}} من {{total}} عنصر",
"overdue": "متأخر",
"planned": "مخطط"
}, },
"timeline": { "timeline": {
"activityTimeline": "جدول الأنشطة", "activityTimeline": "جدول الأنشطة",
@ -782,7 +784,9 @@
"planned_desc": "تم تحديد نطاقها وجاهزة للبدء", "planned_desc": "تم تحديد نطاقها وجاهزة للبدء",
"in_progress_desc": "يتم العمل النشط", "in_progress_desc": "يتم العمل النشط",
"blocked_desc": "مؤقتًا متوقف أو عالق", "blocked_desc": "مؤقتًا متوقف أو عالق",
"completed_desc": "تم الانتهاء منها" "completed_desc": "تم الانتهاء منها",
"active": "قيد التنفيذ",
"active_desc": "عمل نشط جارٍ"
}, },
"showMetrics": "عرض المقاييس", "showMetrics": "عرض المقاييس",
"hideMetrics": "إخفاء المقاييس" "hideMetrics": "إخفاء المقاييس"

View file

@ -140,7 +140,9 @@
"noProject": "Без проект", "noProject": "Без проект",
"unknownProject": "Неизвестен проект", "unknownProject": "Неизвестен проект",
"tasks": "задачи", "tasks": "задачи",
"showingItems": "Показване на {{current}} от {{total}} елемента" "showingItems": "Показване на {{current}} от {{total}} елемента",
"overdue": "Просрочено",
"planned": "Планирано"
}, },
"timeline": { "timeline": {
"activityTimeline": "Хронология на активността", "activityTimeline": "Хронология на активността",
@ -782,7 +784,9 @@
"planned_desc": "Определено и готово за стартиране", "planned_desc": "Определено и готово за стартиране",
"in_progress_desc": "Активна работа в ход", "in_progress_desc": "Активна работа в ход",
"blocked_desc": "Временно спряно или блокирано", "blocked_desc": "Временно спряно или блокирано",
"completed_desc": "Завършено и готово" "completed_desc": "Завършено и готово",
"active": "В процес",
"active_desc": "Активна работа в ход"
}, },
"showMetrics": "Покажи метрики", "showMetrics": "Покажи метрики",
"hideMetrics": "Скрий метрики" "hideMetrics": "Скрий метрики"

View file

@ -140,7 +140,9 @@
"noProject": "Intet projekt", "noProject": "Intet projekt",
"unknownProject": "Ukendt projekt", "unknownProject": "Ukendt projekt",
"tasks": "opgaver", "tasks": "opgaver",
"showingItems": "Viser {{current}} af {{total}} elementer" "showingItems": "Viser {{current}} af {{total}} elementer",
"overdue": "Forsinket",
"planned": "Planlagt"
}, },
"timeline": { "timeline": {
"activityTimeline": "Aktivitets Tidslinje", "activityTimeline": "Aktivitets Tidslinje",
@ -782,7 +784,9 @@
"planned_desc": "Afgrænset og klar til at starte", "planned_desc": "Afgrænset og klar til at starte",
"in_progress_desc": "Aktivt arbejde i gang", "in_progress_desc": "Aktivt arbejde i gang",
"blocked_desc": "Midlertidigt pauseret eller fastlåst", "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", "showMetrics": "Vis målinger",
"hideMetrics": "Skjul målinger" "hideMetrics": "Skjul målinger"

View file

@ -99,8 +99,10 @@
"title": "Aufgaben", "title": "Aufgaben",
"backlog": "Backlog", "backlog": "Backlog",
"inProgress": "In Bearbeitung", "inProgress": "In Bearbeitung",
"overdue": "Überfällig",
"planned": "Geplant",
"dueToday": "Heute fällig", "dueToday": "Heute fällig",
"completedToday": "Heute abgeschlossen", "completedToday": "Abgeschlossen",
"myPlanToday": "Mein Plan für Heute", "myPlanToday": "Mein Plan für Heute",
"noPlanToday": "Noch keine Aufgaben für heute geplant", "noPlanToday": "Noch keine Aufgaben für heute geplant",
"addToPlanHint": "Verwende die Kalendersymbole neben den Aufgaben, um sie zu deinem heutigen Plan hinzuzufügen", "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", "planned_desc": "Definiert und bereit zum Start",
"in_progress_desc": "Aktive Arbeit im Gange", "in_progress_desc": "Aktive Arbeit im Gange",
"blocked_desc": "Vorübergehend pausiert oder festgefahren", "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", "showMetrics": "Metriken anzeigen",
"hideMetrics": "Metriken ausblenden" "hideMetrics": "Metriken ausblenden"

View file

@ -350,7 +350,9 @@
"noProject": "Χωρίς έργο", "noProject": "Χωρίς έργο",
"unknownProject": "Άγνωστο έργο", "unknownProject": "Άγνωστο έργο",
"tasks": "εργασίες", "tasks": "εργασίες",
"showingItems": "Εμφάνιση {{current}} από {{total}} στοιχεία" "showingItems": "Εμφάνιση {{current}} από {{total}} στοιχεία",
"overdue": "Υπερβολικό",
"planned": "Προγραμματισμένο"
}, },
"timeline": { "timeline": {
"activityTimeline": "Χρονοδιάγραμμα Δραστηριότητας", "activityTimeline": "Χρονοδιάγραμμα Δραστηριότητας",
@ -420,7 +422,9 @@
"planned_desc": "Καθορισμένο και έτοιμο να ξεκινήσει", "planned_desc": "Καθορισμένο και έτοιμο να ξεκινήσει",
"in_progress_desc": "Ενεργή εργασία σε εξέλιξη", "in_progress_desc": "Ενεργή εργασία σε εξέλιξη",
"blocked_desc": "Προσωρινά παγωμένο ή κολλημένο", "blocked_desc": "Προσωρινά παγωμένο ή κολλημένο",
"completed_desc": "Ολοκληρώθηκε και τελείωσε" "completed_desc": "Ολοκληρώθηκε και τελείωσε",
"active": "Σε Εξέλιξη",
"active_desc": "Ενεργή εργασία που πραγματοποιείται"
}, },
"showMetrics": "Εμφάνιση μετρήσεων", "showMetrics": "Εμφάνιση μετρήσεων",
"hideMetrics": "Απόκρυψη μετρήσεων" "hideMetrics": "Απόκρυψη μετρήσεων"

View file

@ -100,10 +100,12 @@
"today": "Today", "today": "Today",
"backlog": "Backlog", "backlog": "Backlog",
"inProgress": "In Progress", "inProgress": "In Progress",
"overdue": "Overdue",
"planned": "Planned",
"dueToday": "Due Today", "dueToday": "Due Today",
"stale": "Stale", "stale": "Stale",
"suggested": "Suggested", "suggested": "Suggested",
"completedToday": "Completed Today", "completedToday": "Completed",
"weeklyCompletions": "Weekly Progress", "weeklyCompletions": "Weekly Progress",
"taskCompleted": "task completed", "taskCompleted": "task completed",
"tasksCompleted": "tasks completed", "tasksCompleted": "tasks completed",

View file

@ -310,10 +310,12 @@
"today": "Hoy", "today": "Hoy",
"backlog": "Pendientes", "backlog": "Pendientes",
"inProgress": "En Progreso", "inProgress": "En Progreso",
"overdue": "Vencidas",
"planned": "Planificadas",
"dueToday": "Vence Hoy", "dueToday": "Vence Hoy",
"stale": "Atrasados", "stale": "Atrasados",
"suggested": "Sugeridos", "suggested": "Sugeridos",
"completedToday": "Completadas Hoy", "completedToday": "Completadas",
"noTasksAvailable": "No hay tareas disponibles para hoy.", "noTasksAvailable": "No hay tareas disponibles para hoy.",
"searchPlaceholder": "Buscar tareas...", "searchPlaceholder": "Buscar tareas...",
"addNewTask": "Añadir Nueva Tarea", "addNewTask": "Añadir Nueva Tarea",
@ -420,7 +422,9 @@
"planned_desc": "Definido y listo para comenzar", "planned_desc": "Definido y listo para comenzar",
"in_progress_desc": "Trabajo activo en curso", "in_progress_desc": "Trabajo activo en curso",
"blocked_desc": "Pausado temporalmente o atascado", "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", "showMetrics": "Mostrar métricas",
"hideMetrics": "Ocultar métricas" "hideMetrics": "Ocultar métricas"

View file

@ -140,7 +140,9 @@
"noProject": "Ei projektia", "noProject": "Ei projektia",
"unknownProject": "Tuntematon projekti", "unknownProject": "Tuntematon projekti",
"tasks": "tehtävät", "tasks": "tehtävät",
"showingItems": "Näytetään {{current}} / {{total}} kohdetta" "showingItems": "Näytetään {{current}} / {{total}} kohdetta",
"overdue": "Erääntynyt",
"planned": "Suunniteltu"
}, },
"timeline": { "timeline": {
"activityTimeline": "Toiminta-aikajana", "activityTimeline": "Toiminta-aikajana",
@ -782,7 +784,9 @@
"planned_desc": "Määritelty ja valmis aloittamaan", "planned_desc": "Määritelty ja valmis aloittamaan",
"in_progress_desc": "Aktiivista työtä käynnissä", "in_progress_desc": "Aktiivista työtä käynnissä",
"blocked_desc": "Tilapäisesti keskeytetty tai jumissa", "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", "showMetrics": "Näytä mittarit",
"hideMetrics": "Piilota mittarit" "hideMetrics": "Piilota mittarit"

View file

@ -100,10 +100,12 @@
"today": "Aujourd'hui", "today": "Aujourd'hui",
"backlog": "Retard", "backlog": "Retard",
"inProgress": "En Cours", "inProgress": "En Cours",
"overdue": "En Retard",
"planned": "Planifié",
"dueToday": "Échéance Aujourd'hui", "dueToday": "Échéance Aujourd'hui",
"stale": "Obsolète", "stale": "Obsolète",
"suggested": "Suggéré", "suggested": "Suggéré",
"completedToday": "Terminé Aujourd'hui", "completedToday": "Terminé",
"weeklyCompletions": "Progrès Hebdomadaire", "weeklyCompletions": "Progrès Hebdomadaire",
"taskCompleted": "tâche terminée", "taskCompleted": "tâche terminée",
"tasksCompleted": "tâches terminées", "tasksCompleted": "tâches terminées",
@ -782,7 +784,9 @@
"planned_desc": "Défini et prêt à commencer", "planned_desc": "Défini et prêt à commencer",
"in_progress_desc": "Travail actif en cours", "in_progress_desc": "Travail actif en cours",
"blocked_desc": "Temporairement mis en pause ou bloqué", "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", "showMetrics": "Afficher les métriques",
"hideMetrics": "Masquer les métriques" "hideMetrics": "Masquer les métriques"

View file

@ -140,7 +140,9 @@
"noProject": "Tanpa proyek", "noProject": "Tanpa proyek",
"unknownProject": "Proyek tidak dikenal", "unknownProject": "Proyek tidak dikenal",
"tasks": "tugas", "tasks": "tugas",
"showingItems": "Menampilkan {{current}} dari {{total}} item" "showingItems": "Menampilkan {{current}} dari {{total}} item",
"overdue": "Terlambat",
"planned": "Direncanakan"
}, },
"timeline": { "timeline": {
"activityTimeline": "Garis Waktu Aktivitas", "activityTimeline": "Garis Waktu Aktivitas",
@ -782,7 +784,9 @@
"planned_desc": "Sudah ditentukan dan siap untuk dimulai", "planned_desc": "Sudah ditentukan dan siap untuk dimulai",
"in_progress_desc": "Pekerjaan aktif sedang berlangsung", "in_progress_desc": "Pekerjaan aktif sedang berlangsung",
"blocked_desc": "Sementara terhenti atau terjebak", "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", "showMetrics": "Tampilkan metrik",
"hideMetrics": "Sembunyikan metrik" "hideMetrics": "Sembunyikan metrik"

View file

@ -140,7 +140,9 @@
"noProject": "Nessun progetto", "noProject": "Nessun progetto",
"unknownProject": "Progetto sconosciuto", "unknownProject": "Progetto sconosciuto",
"tasks": "attività", "tasks": "attività",
"showingItems": "Visualizzazione di {{current}} su {{total}} elementi" "showingItems": "Visualizzazione di {{current}} su {{total}} elementi",
"overdue": "Scaduto",
"planned": "Pianificato"
}, },
"timeline": { "timeline": {
"activityTimeline": "Timeline delle Attività", "activityTimeline": "Timeline delle Attività",
@ -782,7 +784,9 @@
"planned_desc": "Definito e pronto per iniziare", "planned_desc": "Definito e pronto per iniziare",
"in_progress_desc": "Lavoro attivo in corso", "in_progress_desc": "Lavoro attivo in corso",
"blocked_desc": "Pausa temporanea o bloccato", "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", "showMetrics": "Mostra metriche",
"hideMetrics": "Nascondi metriche" "hideMetrics": "Nascondi metriche"

View file

@ -140,7 +140,9 @@
"noProject": "プロジェクトなし", "noProject": "プロジェクトなし",
"unknownProject": "不明なプロジェクト", "unknownProject": "不明なプロジェクト",
"tasks": "タスク", "tasks": "タスク",
"showingItems": "{{total}}件中{{current}}件を表示" "showingItems": "{{total}}件中{{current}}件を表示",
"overdue": "期限切れ",
"planned": "計画中"
}, },
"timeline": { "timeline": {
"activityTimeline": "アクティビティタイムライン", "activityTimeline": "アクティビティタイムライン",
@ -580,7 +582,9 @@
"planned_desc": "スコープが決まり、開始準備が整った", "planned_desc": "スコープが決まり、開始準備が整った",
"in_progress_desc": "アクティブな作業が行われている", "in_progress_desc": "アクティブな作業が行われている",
"blocked_desc": "一時的に停止または行き詰まっている", "blocked_desc": "一時的に停止または行き詰まっている",
"completed_desc": "完了し、終了した" "completed_desc": "完了し、終了した",
"active": "進行中",
"active_desc": "アクティブな作業が行われています"
}, },
"showMetrics": "メトリクスを表示", "showMetrics": "メトリクスを表示",
"hideMetrics": "メトリクスを非表示" "hideMetrics": "メトリクスを非表示"

View file

@ -140,7 +140,9 @@
"noProject": "프로젝트 없음", "noProject": "프로젝트 없음",
"unknownProject": "알 수 없는 프로젝트", "unknownProject": "알 수 없는 프로젝트",
"tasks": "작업", "tasks": "작업",
"showingItems": "{{total}}개 중 {{current}}개 표시" "showingItems": "{{total}}개 중 {{current}}개 표시",
"overdue": "연체",
"planned": "계획됨"
}, },
"timeline": { "timeline": {
"activityTimeline": "활동 타임라인", "activityTimeline": "활동 타임라인",
@ -782,7 +784,9 @@
"planned_desc": "범위가 정의되고 시작할 준비가 됨", "planned_desc": "범위가 정의되고 시작할 준비가 됨",
"in_progress_desc": "활동적인 작업 진행 중", "in_progress_desc": "활동적인 작업 진행 중",
"blocked_desc": "일시적으로 중단되거나 막힘", "blocked_desc": "일시적으로 중단되거나 막힘",
"completed_desc": "완료되고 끝남" "completed_desc": "완료되고 끝남",
"active": "진행 중",
"active_desc": "활동 중인 작업"
}, },
"showMetrics": "메트릭 표시", "showMetrics": "메트릭 표시",
"hideMetrics": "메트릭 숨기기" "hideMetrics": "메트릭 숨기기"

View file

@ -140,7 +140,9 @@
"noProject": "Geen project", "noProject": "Geen project",
"unknownProject": "Onbekend project", "unknownProject": "Onbekend project",
"tasks": "taken", "tasks": "taken",
"showingItems": "{{current}} van {{total}} items weergegeven" "showingItems": "{{current}} van {{total}} items weergegeven",
"overdue": "Te laat",
"planned": "Gepland"
}, },
"timeline": { "timeline": {
"activityTimeline": "Activiteit Tijdlijn", "activityTimeline": "Activiteit Tijdlijn",
@ -782,7 +784,9 @@
"planned_desc": "Afgebakend en klaar om te starten", "planned_desc": "Afgebakend en klaar om te starten",
"in_progress_desc": "Actief werk aan de gang", "in_progress_desc": "Actief werk aan de gang",
"blocked_desc": "Tijdelijk gepauzeerd of vastgelopen", "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", "showMetrics": "Toon statistieken",
"hideMetrics": "Verberg statistieken" "hideMetrics": "Verberg statistieken"

View file

@ -140,7 +140,9 @@
"noProject": "Ingen prosjekt", "noProject": "Ingen prosjekt",
"unknownProject": "Ukjent prosjekt", "unknownProject": "Ukjent prosjekt",
"tasks": "oppgaver", "tasks": "oppgaver",
"showingItems": "Viser {{current}} av {{total}} elementer" "showingItems": "Viser {{current}} av {{total}} elementer",
"overdue": "Forsinket",
"planned": "Planlagt"
}, },
"timeline": { "timeline": {
"activityTimeline": "Aktivitetslinje", "activityTimeline": "Aktivitetslinje",
@ -782,7 +784,9 @@
"planned_desc": "Avgrenset og klar til å starte", "planned_desc": "Avgrenset og klar til å starte",
"in_progress_desc": "Aktivt arbeid pågår", "in_progress_desc": "Aktivt arbeid pågår",
"blocked_desc": "Midlertidig pauset eller fastlåst", "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", "showMetrics": "Vis målinger",
"hideMetrics": "Skjul målinger" "hideMetrics": "Skjul målinger"

View file

@ -140,7 +140,9 @@
"noProject": "Brak projektu", "noProject": "Brak projektu",
"unknownProject": "Nieznany projekt", "unknownProject": "Nieznany projekt",
"tasks": "zadania", "tasks": "zadania",
"showingItems": "Wyświetlanie {{current}} z {{total}} elementów" "showingItems": "Wyświetlanie {{current}} z {{total}} elementów",
"overdue": "Przeterminowane",
"planned": "Zaplanowane"
}, },
"timeline": { "timeline": {
"activityTimeline": "Oś czasu aktywności", "activityTimeline": "Oś czasu aktywności",
@ -782,7 +784,9 @@
"planned_desc": "Określone i gotowe do rozpoczęcia", "planned_desc": "Określone i gotowe do rozpoczęcia",
"in_progress_desc": "Aktywna praca w toku", "in_progress_desc": "Aktywna praca w toku",
"blocked_desc": "Tymczasowo wstrzymane lub utknęło", "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", "showMetrics": "Pokaż metryki",
"hideMetrics": "Ukryj metryki" "hideMetrics": "Ukryj metryki"

View file

@ -140,7 +140,9 @@
"noProject": "Sem projeto", "noProject": "Sem projeto",
"unknownProject": "Projeto desconhecido", "unknownProject": "Projeto desconhecido",
"tasks": "tarefas", "tasks": "tarefas",
"showingItems": "Mostrando {{current}} de {{total}} itens" "showingItems": "Mostrando {{current}} de {{total}} itens",
"overdue": "Atrasado",
"planned": "Planejado"
}, },
"timeline": { "timeline": {
"activityTimeline": "Linha do Tempo de Atividades", "activityTimeline": "Linha do Tempo de Atividades",
@ -782,7 +784,9 @@
"planned_desc": "Escopado e pronto para começar", "planned_desc": "Escopado e pronto para começar",
"in_progress_desc": "Trabalho ativo em andamento", "in_progress_desc": "Trabalho ativo em andamento",
"blocked_desc": "Pausado temporariamente ou preso", "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", "showMetrics": "Mostrar métricas",
"hideMetrics": "Ocultar métricas" "hideMetrics": "Ocultar métricas"

View file

@ -140,7 +140,9 @@
"noProject": "Fără proiect", "noProject": "Fără proiect",
"unknownProject": "Proiect necunoscut", "unknownProject": "Proiect necunoscut",
"tasks": "sarcini", "tasks": "sarcini",
"showingItems": "Se afișează {{current}} din {{total}} elemente" "showingItems": "Se afișează {{current}} din {{total}} elemente",
"overdue": "Întârziat",
"planned": "Planificat"
}, },
"timeline": { "timeline": {
"activityTimeline": "Cronologia activităților", "activityTimeline": "Cronologia activităților",
@ -782,7 +784,9 @@
"planned_desc": "Definit și gata de început", "planned_desc": "Definit și gata de început",
"in_progress_desc": "Lucru activ în desfășurare", "in_progress_desc": "Lucru activ în desfășurare",
"blocked_desc": "Pauză temporară sau blocat", "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", "showMetrics": "Arată metrici",
"hideMetrics": "Ascunde metrici" "hideMetrics": "Ascunde metrici"

View file

@ -140,7 +140,9 @@
"noProject": "Без проекта", "noProject": "Без проекта",
"unknownProject": "Неизвестный проект", "unknownProject": "Неизвестный проект",
"tasks": "задачи", "tasks": "задачи",
"showingItems": "Показано {{current}} из {{total}} элементов" "showingItems": "Показано {{current}} из {{total}} элементов",
"overdue": "Просрочено",
"planned": "Запланировано"
}, },
"timeline": { "timeline": {
"activityTimeline": "Хронология активности", "activityTimeline": "Хронология активности",
@ -782,7 +784,9 @@
"planned_desc": "Определено и готово к началу", "planned_desc": "Определено и готово к началу",
"in_progress_desc": "Активная работа идет", "in_progress_desc": "Активная работа идет",
"blocked_desc": "Временно приостановлено или застряло", "blocked_desc": "Временно приостановлено или застряло",
"completed_desc": "Завершено и выполнено" "completed_desc": "Завершено и выполнено",
"active": "В процессе",
"active_desc": "Активная работа ведется"
}, },
"showMetrics": "Показать метрики", "showMetrics": "Показать метрики",
"hideMetrics": "Скрыть метрики" "hideMetrics": "Скрыть метрики"

View file

@ -140,7 +140,9 @@
"noProject": "Brez projekta", "noProject": "Brez projekta",
"unknownProject": "Neznan projekt", "unknownProject": "Neznan projekt",
"tasks": "naloge", "tasks": "naloge",
"showingItems": "Prikazovanje {{current}} od {{total}} elementov" "showingItems": "Prikazovanje {{current}} od {{total}} elementov",
"overdue": "Zapadlo",
"planned": "Načrtovano"
}, },
"timeline": { "timeline": {
"activityTimeline": "Časovnica aktivnosti", "activityTimeline": "Časovnica aktivnosti",
@ -782,7 +784,9 @@
"planned_desc": "Določeno in pripravljeno za začetek", "planned_desc": "Določeno in pripravljeno za začetek",
"in_progress_desc": "Aktivno delo poteka", "in_progress_desc": "Aktivno delo poteka",
"blocked_desc": "Začasno ustavljeno ali zastojev", "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", "showMetrics": "Prikaži metrike",
"hideMetrics": "Skrij metrike" "hideMetrics": "Skrij metrike"

View file

@ -140,7 +140,9 @@
"noProject": "Inget projekt", "noProject": "Inget projekt",
"unknownProject": "Okänt projekt", "unknownProject": "Okänt projekt",
"tasks": "uppgifter", "tasks": "uppgifter",
"showingItems": "Visar {{current}} av {{total}} objekt" "showingItems": "Visar {{current}} av {{total}} objekt",
"overdue": "Förfallen",
"planned": "Planerad"
}, },
"timeline": { "timeline": {
"activityTimeline": "Aktivitetslinje", "activityTimeline": "Aktivitetslinje",
@ -782,7 +784,9 @@
"planned_desc": "Avgränsad och redo att starta", "planned_desc": "Avgränsad och redo att starta",
"in_progress_desc": "Aktivt arbete pågår", "in_progress_desc": "Aktivt arbete pågår",
"blocked_desc": "Tillfälligt pausad eller fast", "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", "showMetrics": "Visa mätvärden",
"hideMetrics": "Dölj mätvärden" "hideMetrics": "Dölj mätvärden"

View file

@ -140,7 +140,9 @@
"noProject": "Proje yok", "noProject": "Proje yok",
"unknownProject": "Bilinmeyen proje", "unknownProject": "Bilinmeyen proje",
"tasks": "görevler", "tasks": "görevler",
"showingItems": "{{total}} öğeden {{current}} tanesi gösteriliyor" "showingItems": "{{total}} öğeden {{current}} tanesi gösteriliyor",
"overdue": "Gecikmiş",
"planned": "Planlanmış"
}, },
"timeline": { "timeline": {
"activityTimeline": "Etkinlik Zaman Çizelgesi", "activityTimeline": "Etkinlik Zaman Çizelgesi",
@ -782,7 +784,9 @@
"planned_desc": "Kapsam belirlendi ve başlamaya hazır", "planned_desc": "Kapsam belirlendi ve başlamaya hazır",
"in_progress_desc": "Aktif çalışma devam ediyor", "in_progress_desc": "Aktif çalışma devam ediyor",
"blocked_desc": "Geçici olarak duraklatıldı veya takıldı", "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", "showMetrics": "Metrikleri göster",
"hideMetrics": "Metrikleri gizle" "hideMetrics": "Metrikleri gizle"

View file

@ -140,7 +140,9 @@
"noProject": "Без проекту", "noProject": "Без проекту",
"unknownProject": "Невідомий проект", "unknownProject": "Невідомий проект",
"tasks": "завдання", "tasks": "завдання",
"showingItems": "Показано {{current}} з {{total}} елементів" "showingItems": "Показано {{current}} з {{total}} елементів",
"overdue": "Прострочено",
"planned": "Заплановано"
}, },
"timeline": { "timeline": {
"activityTimeline": "Хронологія Активності", "activityTimeline": "Хронологія Активності",
@ -210,7 +212,9 @@
"planned_desc": "Визначено та готово до початку", "planned_desc": "Визначено та готово до початку",
"in_progress_desc": "Активна робота триває", "in_progress_desc": "Активна робота триває",
"blocked_desc": "Тимчасово призупинено або застрягло", "blocked_desc": "Тимчасово призупинено або застрягло",
"completed_desc": "Завершено та виконано" "completed_desc": "Завершено та виконано",
"active": "В процесі",
"active_desc": "Активна робота триває"
}, },
"showMetrics": "Показати метрики", "showMetrics": "Показати метрики",
"hideMetrics": "Сховати метрики" "hideMetrics": "Сховати метрики"

View file

@ -140,7 +140,9 @@
"noProject": "Không có dự án", "noProject": "Không có dự án",
"unknownProject": "Dự án không xác định", "unknownProject": "Dự án không xác định",
"tasks": "nhiệm vụ", "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": { "timeline": {
"activityTimeline": "Dòng Thời Gian Hoạt Động", "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", "planned_desc": "Đã xác định và sẵn sàng bắt đầu",
"in_progress_desc": "Công việc đang diễn ra", "in_progress_desc": "Công việc đang diễn ra",
"blocked_desc": "Tạm dừng hoặc bị mắc kẹt", "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", "showMetrics": "Hiển thị số liệu",
"hideMetrics": "Ẩn số liệu" "hideMetrics": "Ẩn số liệu"

View file

@ -140,7 +140,9 @@
"noProject": "无项目", "noProject": "无项目",
"unknownProject": "未知项目", "unknownProject": "未知项目",
"tasks": "任务", "tasks": "任务",
"showingItems": "显示 {{current}} / {{total}} 项" "showingItems": "显示 {{current}} / {{total}} 项",
"overdue": "逾期",
"planned": "计划中"
}, },
"timeline": { "timeline": {
"activityTimeline": "活动时间线", "activityTimeline": "活动时间线",
@ -782,7 +784,9 @@
"planned_desc": "已确定范围并准备开始", "planned_desc": "已确定范围并准备开始",
"in_progress_desc": "正在进行的工作", "in_progress_desc": "正在进行的工作",
"blocked_desc": "暂时暂停或卡住", "blocked_desc": "暂时暂停或卡住",
"completed_desc": "已完成并结束" "completed_desc": "已完成并结束",
"active": "进行中",
"active_desc": "正在进行的工作"
}, },
"showMetrics": "显示指标", "showMetrics": "显示指标",
"hideMetrics": "隐藏指标" "hideMetrics": "隐藏指标"