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 = [
'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) {

View file

@ -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 = {

View file

@ -47,7 +47,9 @@ async function fetchTasksInProgress(visibleTasksWhere) {
async function fetchTodayPlanTasks(visibleTasksWhere) {
return await Task.findAll({
where: {
...visibleTasksWhere,
[Op.and]: [
visibleTasksWhere,
{
today: true,
status: {
[Op.notIn]: [
@ -60,6 +62,8 @@ async function fetchTodayPlanTasks(visibleTasksWhere) {
parent_task_id: null,
recurring_parent_id: null,
},
],
},
include: getTaskIncludeConfig(),
order: [
['priority', 'DESC'],
@ -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,

View file

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

View file

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

View file

@ -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
'

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,
expect: { timeout: 10_000 },
fullyParallel: true,
workers: process.env.CI ? 1 : undefined, // Use default workers locally, 1 in CI
reporter: [['list']],
use: {
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');
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
)
) {
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,21 +1264,145 @@ const TasksToday: React.FC = () => {
</div>
) : null}
{/* Overdue Tasks - Displayed first */}
{isSettingsLoaded &&
todaySettings.showDueToday &&
metrics.tasks_overdue.length > 0 && (
<div className="mb-6" data-testid="overdue-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={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 Overdue Tasks */}
{overdueDisplayLimit <
metrics.tasks_overdue.length && (
<div className="flex justify-center pt-4 pb-2 gap-3">
<button
onClick={() =>
setOverdueDisplayLimit(
(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={() =>
setOverdueDisplayLimit(
metrics.tasks_overdue
.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 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>
</>
)}
</div>
)}
{/* Today Plan */}
{(metrics.today_plan_tasks || []).length > 0 && (
<div className="mb-6" data-testid="planned-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={toggleTodayPlanCollapsed}
data-testid="planned-section-header"
>
<h3 className="text-xl font-medium">
{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 || []}
todayPlanTasks={
metrics.today_plan_tasks || []
}
projects={localProjects}
onTaskUpdate={handleTaskUpdate}
onTaskDelete={handleTaskDelete}
onToggleToday={handleToggleToday}
onTaskCompletionToggle={handleTaskCompletionToggle}
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)}
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"
>
@ -1240,12 +1428,18 @@ const TasksToday: React.FC = () => {
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...')}
{t(
'common.loading',
'Loading...'
)}
</>
) : (
<>
<QueueListIcon className="h-4 w-4 mr-2" />
{t('common.loadMore', 'Load More')}
{t(
'common.loadMore',
'Load More'
)}
</>
)}
</button>
@ -1260,18 +1454,117 @@ const TasksToday: React.FC = () => {
)}
{/* Pagination info for Today Plan tasks */}
{(metrics.today_plan_tasks || []).length > 0 && (
<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,
current: (
metrics.today_plan_tasks || []
).length,
total: pagination.total,
}
)}
</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 */}
@ -1319,83 +1612,20 @@ const TasksToday: React.FC = () => {
</div>
) : 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 */}
{isSettingsLoaded &&
todaySettings.showCompleted &&
(() => {
const completedToday = metrics.tasks_completed_today; // Use the already filtered list from backend
return (
<div className="mb-6">
<div
className="mb-6"
data-testid="completed-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={toggleCompletedCollapsed}
data-testid="completed-section-header"
>
<h3 className="text-xl font-medium">
{t('tasks.completedToday')}

View file

@ -35,17 +35,66 @@ const TodayPlan: React.FC<TodayPlanProps> = ({
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
}
// If both are not in progress, maintain original order
// 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, 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

View file

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

60
package-lock.json generated
View file

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

View file

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

View file

@ -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": "إخفاء المقاييس"

View file

@ -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": "Скрий метрики"

View file

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

View file

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

View file

@ -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": "Απόκρυψη μετρήσεων"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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": "メトリクスを非表示"

View file

@ -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": "메트릭 숨기기"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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": "Скрыть метрики"

View file

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

View file

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

View file

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

View file

@ -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": "Сховати метрики"

View file

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

View file

@ -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": "隐藏指标"