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:
parent
f663ad5d52
commit
2d2a989a5f
41 changed files with 1041 additions and 357 deletions
0
backend/database.sqlite
Normal file
0
backend/database.sqlite
Normal 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) {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -131,7 +131,6 @@ async function filterTasksByParams(
|
|||
{ recurrence_type: { [Op.ne]: 'none' } },
|
||||
{ recurrence_type: { [Op.ne]: null } },
|
||||
{ recurring_parent_id: null },
|
||||
{ today: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
92
e2e/package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
245
e2e/tests/today-view.spec.ts
Normal file
245
e2e/tests/today-view.spec.ts
Normal 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
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
60
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "إخفاء المقاييس"
|
||||
|
|
|
|||
|
|
@ -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": "Скрий метрики"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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": "Απόκρυψη μετρήσεων"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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": "メトリクスを非表示"
|
||||
|
|
|
|||
|
|
@ -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": "메트릭 숨기기"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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": "Скрыть метрики"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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": "Сховати метрики"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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": "隐藏指标"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue