From c94276eab760401c82928c9200f8e306ddbe9bb1 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:02:50 +0100 Subject: [PATCH 01/18] Add GetByDueDateRangeAsync to ICardRepository New repository method to query cards with due dates falling within a specified date range across multiple boards, supporting the calendar view feature. --- .../Taskdeck.Application/Interfaces/ICardRepository.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/src/Taskdeck.Application/Interfaces/ICardRepository.cs b/backend/src/Taskdeck.Application/Interfaces/ICardRepository.cs index 6522e2dbe..bae234f30 100644 --- a/backend/src/Taskdeck.Application/Interfaces/ICardRepository.cs +++ b/backend/src/Taskdeck.Application/Interfaces/ICardRepository.cs @@ -41,4 +41,14 @@ Task> GetBlockedByBoardIdAsync( Guid boardId, Guid? labelId = null, CancellationToken cancellationToken = default); + + /// + /// Get cards with due dates falling within the specified date range across multiple boards. + /// Returns cards ordered by due date ascending. + /// + Task> GetByDueDateRangeAsync( + IEnumerable boardIds, + DateTimeOffset from, + DateTimeOffset to, + CancellationToken cancellationToken = default); } From 9d125ac060a08e10c8a4ae64074731e7abada4b4 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:02:56 +0100 Subject: [PATCH 02/18] Implement GetByDueDateRangeAsync in CardRepository SQL-level query for cards with due dates in a range across boards. Uses AsNoTracking, includes Board and Column navigation properties, ordered by DueDate ascending. --- .../Repositories/CardRepository.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/backend/src/Taskdeck.Infrastructure/Repositories/CardRepository.cs b/backend/src/Taskdeck.Infrastructure/Repositories/CardRepository.cs index d15973a55..b80baf2ec 100644 --- a/backend/src/Taskdeck.Infrastructure/Repositories/CardRepository.cs +++ b/backend/src/Taskdeck.Infrastructure/Repositories/CardRepository.cs @@ -237,4 +237,32 @@ public async Task> GetBlockedByBoardIdAsync( return await query.ToListAsync(cancellationToken); } + + public async Task> GetByDueDateRangeAsync( + IEnumerable boardIds, + DateTimeOffset from, + DateTimeOffset to, + CancellationToken cancellationToken = default) + { + var materializedBoardIds = boardIds + .Where(id => id != Guid.Empty) + .Distinct() + .ToList(); + + if (materializedBoardIds.Count == 0) + return []; + + return await _dbSet + .AsNoTracking() + .Where(c => + materializedBoardIds.Contains(c.BoardId) && + c.DueDate.HasValue && + c.DueDate.Value >= from && + c.DueDate.Value < to) + .Include(c => c.Board) + .Include(c => c.Column) + .OrderBy(c => c.DueDate) + .ThenBy(c => c.BoardId) + .ToListAsync(cancellationToken); + } } From ba7c34f8bb728f8a50e4500208132dcb17389f38 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:03:01 +0100 Subject: [PATCH 03/18] Add calendar DTOs for workspace calendar endpoint WorkspaceCalendarCardDto and WorkspaceCalendarDto provide the response shape for cards with due dates, including overdue status, board/column context, and blocked state. --- .../DTOs/WorkspaceDtos.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/backend/src/Taskdeck.Application/DTOs/WorkspaceDtos.cs b/backend/src/Taskdeck.Application/DTOs/WorkspaceDtos.cs index f070e8223..ccc5e7bfb 100644 --- a/backend/src/Taskdeck.Application/DTOs/WorkspaceDtos.cs +++ b/backend/src/Taskdeck.Application/DTOs/WorkspaceDtos.cs @@ -85,3 +85,22 @@ public record WorkspaceNextActionDto( string TargetSurface, Guid? BoardId = null, int? AttentionCount = null); + +public record WorkspaceCalendarCardDto( + Guid CardId, + Guid BoardId, + string BoardName, + Guid ColumnId, + string ColumnName, + string Title, + DateTimeOffset DueDate, + bool IsBlocked, + string? BlockReason, + bool IsOverdue, + DateTimeOffset UpdatedAt); + +public record WorkspaceCalendarDto( + DateTimeOffset From, + DateTimeOffset To, + int TotalCards, + IReadOnlyList Cards); From b20be60412bdb9c9a60cee6c1d11bd0880758c36 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:03:07 +0100 Subject: [PATCH 04/18] Add GetCalendarAsync to workspace service Queries cards with due dates across accessible boards for a date range. Caps range at 90 days, validates from < to, and computes overdue status relative to current UTC time. --- .../Services/IWorkspaceService.cs | 5 ++ .../Services/WorkspaceService.cs | 56 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/backend/src/Taskdeck.Application/Services/IWorkspaceService.cs b/backend/src/Taskdeck.Application/Services/IWorkspaceService.cs index ea5799f77..7af64a7d0 100644 --- a/backend/src/Taskdeck.Application/Services/IWorkspaceService.cs +++ b/backend/src/Taskdeck.Application/Services/IWorkspaceService.cs @@ -16,4 +16,9 @@ Task> UpdateOnboardingAsync( Guid userId, UpdateWorkspaceOnboardingDto dto, CancellationToken cancellationToken = default); + Task> GetCalendarAsync( + Guid userId, + DateTimeOffset from, + DateTimeOffset to, + CancellationToken cancellationToken = default); } diff --git a/backend/src/Taskdeck.Application/Services/WorkspaceService.cs b/backend/src/Taskdeck.Application/Services/WorkspaceService.cs index 41259f53a..660ca462e 100644 --- a/backend/src/Taskdeck.Application/Services/WorkspaceService.cs +++ b/backend/src/Taskdeck.Application/Services/WorkspaceService.cs @@ -252,6 +252,62 @@ public async Task> UpdateOnboardingAsync( return Result.Success(onboarding); } + public async Task> GetCalendarAsync( + Guid userId, + DateTimeOffset from, + DateTimeOffset to, + CancellationToken cancellationToken = default) + { + if (userId == Guid.Empty) + return Result.Failure(ErrorCodes.ValidationError, "User ID cannot be empty"); + + if (from >= to) + return Result.Failure(ErrorCodes.ValidationError, "The 'from' date must be before the 'to' date"); + + // Cap range to 90 days to prevent unbounded queries + var maxRange = TimeSpan.FromDays(90); + if (to - from > maxRange) + return Result.Failure(ErrorCodes.ValidationError, "Date range cannot exceed 90 days"); + + var accessibleBoards = (await _unitOfWork.Boards.GetReadableByUserIdAsync( + userId, + includeArchived: false, + cancellationToken)) + .ToList(); + + if (accessibleBoards.Count == 0) + { + return Result.Success(new WorkspaceCalendarDto(from, to, 0, [])); + } + + var cards = (await _unitOfWork.Cards.GetByDueDateRangeAsync( + accessibleBoards.Select(b => b.Id), + from, + to, + cancellationToken)) + .ToList(); + + var referenceTime = DateTimeOffset.UtcNow; + + var calendarCards = cards + .Where(c => c.DueDate.HasValue) + .Select(c => new WorkspaceCalendarCardDto( + c.Id, + c.BoardId, + c.Board?.Name ?? "Unknown", + c.ColumnId, + c.Column?.Name ?? "Unknown", + c.Title, + c.DueDate!.Value, + c.IsBlocked, + c.BlockReason, + c.DueDate!.Value < referenceTime, + c.UpdatedAt)) + .ToList(); + + return Result.Success(new WorkspaceCalendarDto(from, to, calendarCards.Count, calendarCards)); + } + private async Task EnsurePreferenceAsync(Guid userId, CancellationToken cancellationToken) { return await _unitOfWork.UserPreferences.GetOrCreateDefaultByUserIdAsync(userId, cancellationToken); From 968571332729d229083dd161dae1eca3a94ce2a0 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:03:12 +0100 Subject: [PATCH 05/18] Add GET /api/workspace/calendar endpoint Returns cards with due dates in a date range across all user-accessible boards. Defaults to current month when no dates provided. Supports calendar and timeline view rendering. --- .../Controllers/WorkspaceController.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/backend/src/Taskdeck.Api/Controllers/WorkspaceController.cs b/backend/src/Taskdeck.Api/Controllers/WorkspaceController.cs index 0a9302661..e3483712d 100644 --- a/backend/src/Taskdeck.Api/Controllers/WorkspaceController.cs +++ b/backend/src/Taskdeck.Api/Controllers/WorkspaceController.cs @@ -74,4 +74,33 @@ public async Task UpdateOnboarding( var result = await _workspaceService.UpdateOnboardingAsync(userId, dto, cancellationToken); return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult(); } + + /// + /// Get cards with due dates within the specified date range across all accessible boards. + /// Returns calendar items suitable for calendar/timeline visualization. + /// + /// Start of the date range (inclusive). Defaults to start of current month. + /// End of the date range (exclusive). Defaults to end of current month. + /// Returns calendar cards for the date range. + /// Invalid date range (from >= to or span > 90 days). + /// Authentication required. + [HttpGet("calendar")] + [ProducesResponseType(typeof(WorkspaceCalendarDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task GetCalendar( + [FromQuery] DateTimeOffset? from, + [FromQuery] DateTimeOffset? to, + CancellationToken cancellationToken = default) + { + if (!TryGetCurrentUserId(out var userId, out var errorResult)) + return errorResult!; + + var now = DateTimeOffset.UtcNow; + var effectiveFrom = from ?? new DateTimeOffset(now.Year, now.Month, 1, 0, 0, 0, TimeSpan.Zero); + var effectiveTo = to ?? effectiveFrom.AddMonths(1); + + var result = await _workspaceService.GetCalendarAsync(userId, effectiveFrom, effectiveTo, cancellationToken); + return result.IsSuccess ? Ok(result.Value) : result.ToErrorActionResult(); + } } From 53ea37cd0b2b01a60ac564b04f960915196993ab Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:06:22 +0100 Subject: [PATCH 06/18] Add CalendarCard and CalendarData types for calendar view Frontend type definitions matching the backend WorkspaceCalendarDto response shape, including card-level overdue/blocked status indicators. --- frontend/taskdeck-web/src/types/workspace.ts | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/frontend/taskdeck-web/src/types/workspace.ts b/frontend/taskdeck-web/src/types/workspace.ts index 7efbc23bc..0674628f1 100644 --- a/frontend/taskdeck-web/src/types/workspace.ts +++ b/frontend/taskdeck-web/src/types/workspace.ts @@ -107,3 +107,24 @@ export interface TodaySummary { blockedCards: TodayAgendaCard[] recommendedActions: HomeRecommendedAction[] } + +export interface CalendarCard { + cardId: string + boardId: string + boardName: string + columnId: string + columnName: string + title: string + dueDate: string + isBlocked: boolean + blockReason: string | null + isOverdue: boolean + updatedAt: string +} + +export interface CalendarData { + from: string + to: string + totalCards: number + cards: CalendarCard[] +} From 1ab036709c4c0355f952ce4b65918bc336d9373c Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:06:29 +0100 Subject: [PATCH 07/18] Add getCalendar method to workspace API client Queries the calendar endpoint with from/to date range parameters. --- frontend/taskdeck-web/src/api/workspaceApi.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/taskdeck-web/src/api/workspaceApi.ts b/frontend/taskdeck-web/src/api/workspaceApi.ts index 502802f42..c979c7a70 100644 --- a/frontend/taskdeck-web/src/api/workspaceApi.ts +++ b/frontend/taskdeck-web/src/api/workspaceApi.ts @@ -1,5 +1,6 @@ import http from './http' import type { + CalendarData, HomeSummary, TodaySummary, UpdateWorkspaceOnboardingDto, @@ -19,6 +20,12 @@ export const workspaceApi = { return data }, + async getCalendar(from: string, to: string): Promise { + const params = new URLSearchParams({ from, to }) + const { data } = await http.get(`/workspace/calendar?${params}`) + return data + }, + async getPreferences(): Promise { const { data } = await http.get('/workspace/preferences') return data From 5975a7436ca3741d874a0c9dd5be7fbfb4a3e6d5 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:06:35 +0100 Subject: [PATCH 08/18] Add CalendarView with grid and timeline modes Monthly calendar grid shows cards by due date with color-coded status indicators (overdue=red, blocked=amber, on-track=green). Timeline mode shows a chronological list grouped by date. Both modes support month navigation and drill-down to board context. --- .../taskdeck-web/src/views/CalendarView.vue | 738 ++++++++++++++++++ 1 file changed, 738 insertions(+) create mode 100644 frontend/taskdeck-web/src/views/CalendarView.vue diff --git a/frontend/taskdeck-web/src/views/CalendarView.vue b/frontend/taskdeck-web/src/views/CalendarView.vue new file mode 100644 index 000000000..f2fa4f62c --- /dev/null +++ b/frontend/taskdeck-web/src/views/CalendarView.vue @@ -0,0 +1,738 @@ + + + + + From 2f9938c35ddf262584942027e78f960cc348eed7 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:06:39 +0100 Subject: [PATCH 09/18] Add /workspace/calendar route for calendar planning view Lazy-loaded CalendarView component with requiresShell meta flag. --- frontend/taskdeck-web/src/router/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/taskdeck-web/src/router/index.ts b/frontend/taskdeck-web/src/router/index.ts index 54d704b30..ba308a05b 100644 --- a/frontend/taskdeck-web/src/router/index.ts +++ b/frontend/taskdeck-web/src/router/index.ts @@ -39,6 +39,7 @@ const ReviewView = () => import('../views/ReviewView.vue') const DevToolsView = () => import('../views/DevToolsView.vue') const SavedViewsView = () => import('../views/SavedViewsView.vue') const MetricsView = () => import('../views/MetricsView.vue') +const CalendarView = () => import('../views/CalendarView.vue') const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -140,6 +141,14 @@ const router = createRouter({ meta: { requiresShell: true }, }, + // Calendar/timeline planning route + { + path: '/workspace/calendar', + name: 'workspace-calendar', + component: CalendarView, + meta: { requiresShell: true }, + }, + // Automation routes { path: '/workspace/automations', From 168112061bb3d779b26c52cf2653fa6f4bac9ee8 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:06:44 +0100 Subject: [PATCH 10/18] Add Calendar nav item to sidebar Visible in workbench mode primary, guided/agent as secondary. Uses 'D' icon mnemonic for due-dates/deadlines. --- .../taskdeck-web/src/components/shell/ShellSidebar.vue | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/taskdeck-web/src/components/shell/ShellSidebar.vue b/frontend/taskdeck-web/src/components/shell/ShellSidebar.vue index d577b4bc9..e146bd76b 100644 --- a/frontend/taskdeck-web/src/components/shell/ShellSidebar.vue +++ b/frontend/taskdeck-web/src/components/shell/ShellSidebar.vue @@ -142,6 +142,16 @@ const navCatalog: NavItem[] = [ secondaryModes: ['guided', 'agent'], keywords: 'chat automation assistant board context', }, + { + id: 'calendar', + label: 'Calendar', + icon: 'D', + path: '/workspace/calendar', + flag: null, + primaryModes: ['workbench'], + secondaryModes: ['guided', 'agent'], + keywords: 'calendar timeline planning due dates schedule deadlines', + }, { id: 'metrics', label: 'Metrics', From 7f83048c717340b74def0d122490319680c7da65 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:09:09 +0100 Subject: [PATCH 11/18] Add GetCalendarAsync unit tests for workspace service Tests validation (empty userId, from>=to, >90 day range), empty result when no boards, card retrieval with due dates, overdue marking, blocked status inclusion, and from/to response fields. --- .../Services/WorkspaceServiceTests.cs | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/backend/tests/Taskdeck.Application.Tests/Services/WorkspaceServiceTests.cs b/backend/tests/Taskdeck.Application.Tests/Services/WorkspaceServiceTests.cs index aef8fbaf6..e78d4bce1 100644 --- a/backend/tests/Taskdeck.Application.Tests/Services/WorkspaceServiceTests.cs +++ b/backend/tests/Taskdeck.Application.Tests/Services/WorkspaceServiceTests.cs @@ -463,4 +463,179 @@ public async Task UpdateOnboardingAsync_ShouldDismissAndReplayState() replayResult.Value.Visibility.Should().Be(WorkspaceOnboardingVisibilityContract.Active); preference.OnboardingDismissedAt.Should().BeNull(); } + + // ── GetCalendarAsync ────────────────────────────────────────────────────────── + + [Fact] + public async Task GetCalendarAsync_ShouldReturnValidationError_WhenUserIdIsEmpty() + { + var result = await _service.GetCalendarAsync( + Guid.Empty, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddDays(30)); + + result.IsSuccess.Should().BeFalse(); + result.ErrorCode.Should().Be(ErrorCodes.ValidationError); + result.ErrorMessage.Should().Contain("User ID cannot be empty"); + } + + [Fact] + public async Task GetCalendarAsync_ShouldReturnValidationError_WhenFromIsAfterTo() + { + var userId = Guid.NewGuid(); + var now = DateTimeOffset.UtcNow; + + var result = await _service.GetCalendarAsync(userId, now.AddDays(10), now); + + result.IsSuccess.Should().BeFalse(); + result.ErrorCode.Should().Be(ErrorCodes.ValidationError); + result.ErrorMessage.Should().Contain("'from' date must be before the 'to' date"); + } + + [Fact] + public async Task GetCalendarAsync_ShouldReturnValidationError_WhenRangeExceeds90Days() + { + var userId = Guid.NewGuid(); + var from = DateTimeOffset.UtcNow; + var to = from.AddDays(91); + + var result = await _service.GetCalendarAsync(userId, from, to); + + result.IsSuccess.Should().BeFalse(); + result.ErrorCode.Should().Be(ErrorCodes.ValidationError); + result.ErrorMessage.Should().Contain("90 days"); + } + + [Fact] + public async Task GetCalendarAsync_ShouldReturnEmptyResult_WhenNoAccessibleBoards() + { + var userId = Guid.NewGuid(); + var from = DateTimeOffset.UtcNow; + var to = from.AddDays(30); + + _boardRepositoryMock + .Setup(r => r.GetReadableByUserIdAsync(userId, false, default)) + .ReturnsAsync([]); + + var result = await _service.GetCalendarAsync(userId, from, to); + + result.IsSuccess.Should().BeTrue(); + result.Value.TotalCards.Should().Be(0); + result.Value.Cards.Should().BeEmpty(); + } + + [Fact] + public async Task GetCalendarAsync_ShouldReturnCardsWithDueDates() + { + var userId = Guid.NewGuid(); + var boardId = Guid.NewGuid(); + var columnId = Guid.NewGuid(); + var from = new DateTimeOffset(2026, 4, 1, 0, 0, 0, TimeSpan.Zero); + var to = new DateTimeOffset(2026, 5, 1, 0, 0, 0, TimeSpan.Zero); + + var board = new Board(boardId, userId, "Test Board"); + var column = new Column(columnId, boardId, "In Progress", 0); + var card = new Card(boardId, columnId, "Test Card", dueDate: new DateTimeOffset(2026, 4, 15, 0, 0, 0, TimeSpan.Zero)); + + _boardRepositoryMock + .Setup(r => r.GetReadableByUserIdAsync(userId, false, default)) + .ReturnsAsync(new List { board }); + + _cardRepositoryMock + .Setup(r => r.GetByDueDateRangeAsync( + It.IsAny>(), + from, + to, + default)) + .ReturnsAsync(new List { card }); + + var result = await _service.GetCalendarAsync(userId, from, to); + + result.IsSuccess.Should().BeTrue(); + result.Value.TotalCards.Should().Be(1); + result.Value.Cards.Should().HaveCount(1); + result.Value.Cards[0].Title.Should().Be("Test Card"); + result.Value.Cards[0].BoardId.Should().Be(boardId); + } + + [Fact] + public async Task GetCalendarAsync_ShouldMarkOverdueCards() + { + var userId = Guid.NewGuid(); + var boardId = Guid.NewGuid(); + var columnId = Guid.NewGuid(); + var from = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero); + var to = new DateTimeOffset(2020, 2, 1, 0, 0, 0, TimeSpan.Zero); + + var board = new Board(boardId, userId, "Test Board"); + var pastDueDate = new DateTimeOffset(2020, 1, 15, 0, 0, 0, TimeSpan.Zero); + var card = new Card(boardId, columnId, "Past Due Card", dueDate: pastDueDate); + + _boardRepositoryMock + .Setup(r => r.GetReadableByUserIdAsync(userId, false, default)) + .ReturnsAsync(new List { board }); + + _cardRepositoryMock + .Setup(r => r.GetByDueDateRangeAsync( + It.IsAny>(), + from, + to, + default)) + .ReturnsAsync(new List { card }); + + var result = await _service.GetCalendarAsync(userId, from, to); + + result.IsSuccess.Should().BeTrue(); + result.Value.Cards[0].IsOverdue.Should().BeTrue(); + } + + [Fact] + public async Task GetCalendarAsync_ShouldIncludeBlockedStatus() + { + var userId = Guid.NewGuid(); + var boardId = Guid.NewGuid(); + var columnId = Guid.NewGuid(); + var from = DateTimeOffset.UtcNow; + var to = from.AddDays(30); + + var board = new Board(boardId, userId, "Test Board"); + var card = new Card(boardId, columnId, "Blocked Card", dueDate: from.AddDays(5)); + card.Block("Waiting on dependency"); + + _boardRepositoryMock + .Setup(r => r.GetReadableByUserIdAsync(userId, false, default)) + .ReturnsAsync(new List { board }); + + _cardRepositoryMock + .Setup(r => r.GetByDueDateRangeAsync( + It.IsAny>(), + from, + to, + default)) + .ReturnsAsync(new List { card }); + + var result = await _service.GetCalendarAsync(userId, from, to); + + result.IsSuccess.Should().BeTrue(); + result.Value.Cards[0].IsBlocked.Should().BeTrue(); + result.Value.Cards[0].BlockReason.Should().Be("Waiting on dependency"); + } + + [Fact] + public async Task GetCalendarAsync_ShouldReturnFromAndToInResponse() + { + var userId = Guid.NewGuid(); + var from = new DateTimeOffset(2026, 4, 1, 0, 0, 0, TimeSpan.Zero); + var to = new DateTimeOffset(2026, 5, 1, 0, 0, 0, TimeSpan.Zero); + + _boardRepositoryMock + .Setup(r => r.GetReadableByUserIdAsync(userId, false, default)) + .ReturnsAsync([]); + + var result = await _service.GetCalendarAsync(userId, from, to); + + result.IsSuccess.Should().BeTrue(); + result.Value.From.Should().Be(from); + result.Value.To.Should().Be(to); + } } From 7b89a03d0f6064cf782a45d6e305e5c1093fae50 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:09:21 +0100 Subject: [PATCH 12/18] Add CalendarView unit tests Coverage for loading/error/empty states, calendar grid rendering, card status indicators (overdue/blocked), month navigation, timeline mode toggle, board drill-down, and block reason display. --- .../src/tests/views/CalendarView.spec.ts | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 frontend/taskdeck-web/src/tests/views/CalendarView.spec.ts diff --git a/frontend/taskdeck-web/src/tests/views/CalendarView.spec.ts b/frontend/taskdeck-web/src/tests/views/CalendarView.spec.ts new file mode 100644 index 000000000..0a9f17bfb --- /dev/null +++ b/frontend/taskdeck-web/src/tests/views/CalendarView.spec.ts @@ -0,0 +1,300 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import CalendarView from '../../views/CalendarView.vue' +import type { CalendarData } from '../../types/workspace' + +const routerMocks = vi.hoisted(() => ({ + push: vi.fn(), +})) + +const mockCalendarData: CalendarData = { + from: '2026-04-01T00:00:00Z', + to: '2026-05-01T00:00:00Z', + totalCards: 3, + cards: [ + { + cardId: 'card-1', + boardId: 'board-1', + boardName: 'Alpha Board', + columnId: 'col-1', + columnName: 'In Progress', + title: 'Ship feature X', + dueDate: '2026-04-10T00:00:00Z', + isBlocked: false, + blockReason: null, + isOverdue: false, + updatedAt: '2026-04-05T12:00:00Z', + }, + { + cardId: 'card-2', + boardId: 'board-2', + boardName: 'Beta Board', + columnId: 'col-2', + columnName: 'Todo', + title: 'Fix urgent bug', + dueDate: '2026-04-03T00:00:00Z', + isBlocked: false, + blockReason: null, + isOverdue: true, + updatedAt: '2026-04-02T12:00:00Z', + }, + { + cardId: 'card-3', + boardId: 'board-1', + boardName: 'Alpha Board', + columnId: 'col-1', + columnName: 'In Progress', + title: 'Blocked task', + dueDate: '2026-04-15T00:00:00Z', + isBlocked: true, + blockReason: 'Waiting on API key', + isOverdue: false, + updatedAt: '2026-04-05T12:00:00Z', + }, + ], +} + +const mockGetCalendar = vi.fn<(from: string, to: string) => Promise>() + +vi.mock('../../api/workspaceApi', () => ({ + workspaceApi: { + getCalendar: (...args: [string, string]) => mockGetCalendar(...args), + }, +})) + +vi.mock('vue-router', () => ({ + useRouter: () => ({ + push: routerMocks.push, + }), +})) + +vi.mock('../../components/workspace/WorkspaceHelpCallout.vue', () => ({ + default: { + template: '
', + props: ['topic', 'title', 'description'], + }, +})) + +async function waitForUi() { + await Promise.resolve() + await Promise.resolve() + await Promise.resolve() +} + +describe('CalendarView', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetCalendar.mockResolvedValue(mockCalendarData) + }) + + it('loads calendar data on mount', async () => { + mount(CalendarView) + await waitForUi() + + expect(mockGetCalendar).toHaveBeenCalledTimes(1) + }) + + it('renders the page title and hero description', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + expect(wrapper.text()).toContain('Calendar') + expect(wrapper.text()).toContain('Planning') + expect(wrapper.text()).toContain('due-date-backed work') + }) + + it('shows loading state while fetching', async () => { + // Never resolve + mockGetCalendar.mockReturnValue(new Promise(() => {})) + const wrapper = mount(CalendarView) + await waitForUi() + + expect(wrapper.text()).toContain('Loading calendar data') + }) + + it('shows error state when fetch fails', async () => { + mockGetCalendar.mockRejectedValue(new Error('Network error')) + const wrapper = mount(CalendarView) + await waitForUi() + + expect(wrapper.text()).toContain('Network error') + }) + + it('shows error state with retry button', async () => { + mockGetCalendar.mockRejectedValue(new Error('Server down')) + const wrapper = mount(CalendarView) + await waitForUi() + + const retryBtn = wrapper.find('.td-btn--ghost.td-btn--sm') + expect(retryBtn.exists()).toBe(true) + expect(retryBtn.text()).toContain('Retry') + }) + + it('shows empty state when no cards have due dates', async () => { + mockGetCalendar.mockResolvedValue({ + from: '2026-04-01T00:00:00Z', + to: '2026-05-01T00:00:00Z', + totalCards: 0, + cards: [], + }) + const wrapper = mount(CalendarView) + await waitForUi() + + expect(wrapper.text()).toContain('No due dates this month') + }) + + it('renders card count for the month', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + expect(wrapper.text()).toContain('3 cards this month') + }) + + it('renders calendar grid with weekday headers', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + const weekdays = wrapper.findAll('.td-calendar__weekday') + expect(weekdays).toHaveLength(7) + expect(weekdays[0].text()).toBe('Sun') + expect(weekdays[6].text()).toBe('Sat') + }) + + it('renders cards in the calendar grid', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + const cardElements = wrapper.findAll('.td-cal-card') + expect(cardElements.length).toBeGreaterThan(0) + expect(wrapper.text()).toContain('Ship feature X') + }) + + it('applies overdue class to overdue cards', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + const overdueCards = wrapper.findAll('.td-cal-card--overdue') + expect(overdueCards.length).toBeGreaterThan(0) + }) + + it('applies blocked class to blocked cards', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + const blockedCards = wrapper.findAll('.td-cal-card--blocked') + expect(blockedCards.length).toBeGreaterThan(0) + }) + + it('navigates to board on card click in calendar grid', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + const card = wrapper.find('.td-cal-card') + await card.trigger('click') + + expect(routerMocks.push).toHaveBeenCalled() + const pushedPath = routerMocks.push.mock.calls[0][0] as string + expect(pushedPath).toMatch(/\/workspace\/boards\/board-/) + }) + + it('switches to timeline view', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + const buttons = wrapper.findAll('.td-calendar__hero-actions .td-btn') + const timelineBtn = buttons.find(b => b.text() === 'Timeline') + expect(timelineBtn).toBeDefined() + await timelineBtn!.trigger('click') + + expect(wrapper.findAll('.td-timeline-group').length).toBeGreaterThan(0) + }) + + it('renders timeline cards with status indicators', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + // Switch to timeline + const buttons = wrapper.findAll('.td-calendar__hero-actions .td-btn') + const timelineBtn = buttons.find(b => b.text() === 'Timeline') + await timelineBtn!.trigger('click') + + expect(wrapper.text()).toContain('Ship feature X') + expect(wrapper.text()).toContain('Fix urgent bug') + expect(wrapper.text()).toContain('Blocked task') + expect(wrapper.text()).toContain('Overdue') + expect(wrapper.text()).toContain('Blocked') + }) + + it('shows block reason in timeline view for blocked cards', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + const buttons = wrapper.findAll('.td-calendar__hero-actions .td-btn') + const timelineBtn = buttons.find(b => b.text() === 'Timeline') + await timelineBtn!.trigger('click') + + expect(wrapper.text()).toContain('Waiting on API key') + }) + + it('renders month navigation', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + const nav = wrapper.find('.td-calendar__nav') + expect(nav.exists()).toBe(true) + expect(nav.text()).toContain('Today') + }) + + it('navigates to next month on arrow click', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + const initialCallCount = mockGetCalendar.mock.calls.length + + const nextBtn = wrapper.findAll('.td-calendar__nav .td-btn--ghost')[1] + await nextBtn.trigger('click') + await waitForUi() + + // Should have fetched again + expect(mockGetCalendar.mock.calls.length).toBeGreaterThan(initialCallCount) + }) + + it('navigates to previous month on arrow click', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + const initialCallCount = mockGetCalendar.mock.calls.length + + const prevBtn = wrapper.findAll('.td-calendar__nav .td-btn--ghost')[0] + await prevBtn.trigger('click') + await waitForUi() + + expect(mockGetCalendar.mock.calls.length).toBeGreaterThan(initialCallCount) + }) + + it('navigates to board from timeline card click', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + const buttons = wrapper.findAll('.td-calendar__hero-actions .td-btn') + const timelineBtn = buttons.find(b => b.text() === 'Timeline') + await timelineBtn!.trigger('click') + + const card = wrapper.find('.td-timeline-card') + await card.trigger('click') + + expect(routerMocks.push).toHaveBeenCalled() + }) + + it('shows board and column names in timeline meta', async () => { + const wrapper = mount(CalendarView) + await waitForUi() + + const buttons = wrapper.findAll('.td-calendar__hero-actions .td-btn') + const timelineBtn = buttons.find(b => b.text() === 'Timeline') + await timelineBtn!.trigger('click') + + expect(wrapper.text()).toContain('Alpha Board') + expect(wrapper.text()).toContain('In Progress') + }) +}) From 46384a1c1183ed4ad78fb9fb59d9d1ed77bab8d9 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:09:26 +0100 Subject: [PATCH 13/18] Update workspace API and route stability tests for calendar Add getCalendar API test and /workspace/calendar to route stability checklist. --- .../src/tests/api/workspaceApi.spec.ts | 18 ++++++++++++++++++ .../router/workspaceRouteStability.spec.ts | 1 + 2 files changed, 19 insertions(+) diff --git a/frontend/taskdeck-web/src/tests/api/workspaceApi.spec.ts b/frontend/taskdeck-web/src/tests/api/workspaceApi.spec.ts index 444d2d628..cca82f4b8 100644 --- a/frontend/taskdeck-web/src/tests/api/workspaceApi.spec.ts +++ b/frontend/taskdeck-web/src/tests/api/workspaceApi.spec.ts @@ -53,4 +53,22 @@ describe('workspaceApi', () => { expect(http.put).toHaveBeenCalledWith('/workspace/onboarding', { action: 'dismiss' }) }) + + it('loads calendar data with from/to parameters', async () => { + vi.mocked(http.get).mockResolvedValue({ data: { from: '2026-04-01', to: '2026-05-01', totalCards: 0, cards: [] } }) + + const from = '2026-04-01T00:00:00.000Z' + const to = '2026-05-01T00:00:00.000Z' + await workspaceApi.getCalendar(from, to) + + expect(http.get).toHaveBeenCalledWith( + expect.stringContaining('/workspace/calendar?'), + ) + expect(http.get).toHaveBeenCalledWith( + expect.stringContaining('from='), + ) + expect(http.get).toHaveBeenCalledWith( + expect.stringContaining('to='), + ) + }) }) diff --git a/frontend/taskdeck-web/src/tests/router/workspaceRouteStability.spec.ts b/frontend/taskdeck-web/src/tests/router/workspaceRouteStability.spec.ts index cc429167e..928c8f01e 100644 --- a/frontend/taskdeck-web/src/tests/router/workspaceRouteStability.spec.ts +++ b/frontend/taskdeck-web/src/tests/router/workspaceRouteStability.spec.ts @@ -428,6 +428,7 @@ describe('unexpected origin and path redirect protection (#687)', () => { '/workspace/boards', '/workspace/boards/board-1', '/workspace/metrics', + '/workspace/calendar', '/workspace/inbox', '/workspace/review', '/workspace/activity', From 5d2cbb15c589fd8745b165a31f6c867b931ed2fa Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:11:57 +0100 Subject: [PATCH 14/18] Fix Board constructor calls in calendar tests Use correct Board(name, description, ownerId) constructor instead of Board(id, userId, name) which does not exist. --- .../Services/WorkspaceServiceTests.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/backend/tests/Taskdeck.Application.Tests/Services/WorkspaceServiceTests.cs b/backend/tests/Taskdeck.Application.Tests/Services/WorkspaceServiceTests.cs index e78d4bce1..bfac477e7 100644 --- a/backend/tests/Taskdeck.Application.Tests/Services/WorkspaceServiceTests.cs +++ b/backend/tests/Taskdeck.Application.Tests/Services/WorkspaceServiceTests.cs @@ -528,14 +528,12 @@ public async Task GetCalendarAsync_ShouldReturnEmptyResult_WhenNoAccessibleBoard public async Task GetCalendarAsync_ShouldReturnCardsWithDueDates() { var userId = Guid.NewGuid(); - var boardId = Guid.NewGuid(); var columnId = Guid.NewGuid(); var from = new DateTimeOffset(2026, 4, 1, 0, 0, 0, TimeSpan.Zero); var to = new DateTimeOffset(2026, 5, 1, 0, 0, 0, TimeSpan.Zero); - var board = new Board(boardId, userId, "Test Board"); - var column = new Column(columnId, boardId, "In Progress", 0); - var card = new Card(boardId, columnId, "Test Card", dueDate: new DateTimeOffset(2026, 4, 15, 0, 0, 0, TimeSpan.Zero)); + var board = new Board("Test Board", ownerId: userId); + var card = new Card(board.Id, columnId, "Test Card", dueDate: new DateTimeOffset(2026, 4, 15, 0, 0, 0, TimeSpan.Zero)); _boardRepositoryMock .Setup(r => r.GetReadableByUserIdAsync(userId, false, default)) @@ -555,21 +553,20 @@ public async Task GetCalendarAsync_ShouldReturnCardsWithDueDates() result.Value.TotalCards.Should().Be(1); result.Value.Cards.Should().HaveCount(1); result.Value.Cards[0].Title.Should().Be("Test Card"); - result.Value.Cards[0].BoardId.Should().Be(boardId); + result.Value.Cards[0].BoardId.Should().Be(board.Id); } [Fact] public async Task GetCalendarAsync_ShouldMarkOverdueCards() { var userId = Guid.NewGuid(); - var boardId = Guid.NewGuid(); var columnId = Guid.NewGuid(); var from = new DateTimeOffset(2020, 1, 1, 0, 0, 0, TimeSpan.Zero); var to = new DateTimeOffset(2020, 2, 1, 0, 0, 0, TimeSpan.Zero); - var board = new Board(boardId, userId, "Test Board"); + var board = new Board("Test Board", ownerId: userId); var pastDueDate = new DateTimeOffset(2020, 1, 15, 0, 0, 0, TimeSpan.Zero); - var card = new Card(boardId, columnId, "Past Due Card", dueDate: pastDueDate); + var card = new Card(board.Id, columnId, "Past Due Card", dueDate: pastDueDate); _boardRepositoryMock .Setup(r => r.GetReadableByUserIdAsync(userId, false, default)) @@ -593,13 +590,12 @@ public async Task GetCalendarAsync_ShouldMarkOverdueCards() public async Task GetCalendarAsync_ShouldIncludeBlockedStatus() { var userId = Guid.NewGuid(); - var boardId = Guid.NewGuid(); var columnId = Guid.NewGuid(); var from = DateTimeOffset.UtcNow; var to = from.AddDays(30); - var board = new Board(boardId, userId, "Test Board"); - var card = new Card(boardId, columnId, "Blocked Card", dueDate: from.AddDays(5)); + var board = new Board("Test Board", ownerId: userId); + var card = new Card(board.Id, columnId, "Blocked Card", dueDate: from.AddDays(5)); card.Block("Waiting on dependency"); _boardRepositoryMock From e6fd4461628e6bbd6046b6ef2d0894c86cff6a88 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 19:13:40 +0100 Subject: [PATCH 15/18] Add 'calendar' to workspace help topics Required for the CalendarView help callout to pass typecheck. --- frontend/taskdeck-web/src/composables/useWorkspaceHelp.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/taskdeck-web/src/composables/useWorkspaceHelp.ts b/frontend/taskdeck-web/src/composables/useWorkspaceHelp.ts index 898c8ed16..0f8311c32 100644 --- a/frontend/taskdeck-web/src/composables/useWorkspaceHelp.ts +++ b/frontend/taskdeck-web/src/composables/useWorkspaceHelp.ts @@ -8,6 +8,7 @@ export const workspaceHelpTopics = [ 'review', 'inbox', 'board', + 'calendar', 'activity-selectors', 'board-access-selectors', 'saved-views', From b916721b5357b076ca67869992338da96c121641 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Thu, 9 Apr 2026 22:51:54 +0100 Subject: [PATCH 16/18] Fix calendar timezone, overdue consistency, and query bounds - Fix todayKey in CalendarView to use UTC methods (getUTCFullYear, getUTCMonth, getUTCDate) matching the grid dateKey construction, preventing wrong "today" highlight in non-UTC timezones - Replace naive DueDate < referenceTime overdue check with existing ResolveDueBucket for consistency with TodayView overdue semantics - Cap GetByDueDateRangeAsync results to 500 to prevent unbounded query responses as data grows - Clear stale calendarData on fetch error to avoid showing outdated card counts in the navigation bar --- backend/src/Taskdeck.Application/Services/WorkspaceService.cs | 2 +- .../src/Taskdeck.Infrastructure/Repositories/CardRepository.cs | 3 +++ frontend/taskdeck-web/src/views/CalendarView.vue | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/Taskdeck.Application/Services/WorkspaceService.cs b/backend/src/Taskdeck.Application/Services/WorkspaceService.cs index 660ca462e..2b92734aa 100644 --- a/backend/src/Taskdeck.Application/Services/WorkspaceService.cs +++ b/backend/src/Taskdeck.Application/Services/WorkspaceService.cs @@ -301,7 +301,7 @@ public async Task> GetCalendarAsync( c.DueDate!.Value, c.IsBlocked, c.BlockReason, - c.DueDate!.Value < referenceTime, + ResolveDueBucket(c.DueDate, referenceTime) == TodayDueBucket.Overdue, c.UpdatedAt)) .ToList(); diff --git a/backend/src/Taskdeck.Infrastructure/Repositories/CardRepository.cs b/backend/src/Taskdeck.Infrastructure/Repositories/CardRepository.cs index b80baf2ec..bab5f4864 100644 --- a/backend/src/Taskdeck.Infrastructure/Repositories/CardRepository.cs +++ b/backend/src/Taskdeck.Infrastructure/Repositories/CardRepository.cs @@ -252,6 +252,8 @@ public async Task> GetByDueDateRangeAsync( if (materializedBoardIds.Count == 0) return []; + const int maxResults = 500; + return await _dbSet .AsNoTracking() .Where(c => @@ -263,6 +265,7 @@ public async Task> GetByDueDateRangeAsync( .Include(c => c.Column) .OrderBy(c => c.DueDate) .ThenBy(c => c.BoardId) + .Take(maxResults) .ToListAsync(cancellationToken); } } diff --git a/frontend/taskdeck-web/src/views/CalendarView.vue b/frontend/taskdeck-web/src/views/CalendarView.vue index f2fa4f62c..bbc33a875 100644 --- a/frontend/taskdeck-web/src/views/CalendarView.vue +++ b/frontend/taskdeck-web/src/views/CalendarView.vue @@ -48,6 +48,7 @@ async function fetchCalendar() { const to = endOfMonth(viewDate.value).toISOString() calendarData.value = await workspaceApi.getCalendar(from, to) } catch (e: unknown) { + calendarData.value = null const msg = e instanceof Error ? e.message : 'Failed to load calendar data' error.value = msg } finally { @@ -83,7 +84,7 @@ const calendarWeeks = computed(() => { const weeks: { date: Date; dateKey: string; isCurrentMonth: boolean; isToday: boolean }[][] = [] const today = new Date() - const todayKey = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}` + const todayKey = `${today.getUTCFullYear()}-${String(today.getUTCMonth() + 1).padStart(2, '0')}-${String(today.getUTCDate()).padStart(2, '0')}` let cursor = new Date(gridStart) while (cursor <= lastDay || weeks.length === 0 || weeks[weeks.length - 1].length < 7) { From 94613cbfcbc95310de94434fff2a65b2af6605ab Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 12 Apr 2026 01:21:58 +0100 Subject: [PATCH 17/18] Fix calendar timezone consistency and accessibility issues - Use UTC accessors (getUTCFullYear, getUTCMonth) in startOfMonth to prevent wrong month selection around timezone boundaries - Rename endOfMonth to startOfNextMonth for clarity (returns exclusive upper bound for date range queries) - Replace button with role="listitem" with proper ul/li semantic structure for timeline cards to improve screen reader compatibility - Add vi.useFakeTimers() in CalendarView tests to freeze time at April 2026, preventing test failures when run in other months Addresses review comments on PR #810. --- .../src/tests/views/CalendarView.spec.ts | 11 ++++++- .../taskdeck-web/src/views/CalendarView.vue | 33 ++++++++++++------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/frontend/taskdeck-web/src/tests/views/CalendarView.spec.ts b/frontend/taskdeck-web/src/tests/views/CalendarView.spec.ts index 0a9f17bfb..9da4f1eed 100644 --- a/frontend/taskdeck-web/src/tests/views/CalendarView.spec.ts +++ b/frontend/taskdeck-web/src/tests/views/CalendarView.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { mount } from '@vue/test-utils' import CalendarView from '../../views/CalendarView.vue' import type { CalendarData } from '../../types/workspace' @@ -7,6 +7,9 @@ const routerMocks = vi.hoisted(() => ({ push: vi.fn(), })) +// Freeze time to April 2026 to match mock data +const MOCK_DATE = new Date('2026-04-05T12:00:00Z') + const mockCalendarData: CalendarData = { from: '2026-04-01T00:00:00Z', to: '2026-05-01T00:00:00Z', @@ -83,10 +86,16 @@ async function waitForUi() { describe('CalendarView', () => { beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(MOCK_DATE) vi.clearAllMocks() mockGetCalendar.mockResolvedValue(mockCalendarData) }) + afterEach(() => { + vi.useRealTimers() + }) + it('loads calendar data on mount', async () => { mount(CalendarView) await waitForUi() diff --git a/frontend/taskdeck-web/src/views/CalendarView.vue b/frontend/taskdeck-web/src/views/CalendarView.vue index bbc33a875..bd4ec0575 100644 --- a/frontend/taskdeck-web/src/views/CalendarView.vue +++ b/frontend/taskdeck-web/src/views/CalendarView.vue @@ -18,11 +18,12 @@ const viewDate = ref(startOfMonth(new Date())) const viewMode = ref<'calendar' | 'timeline'>('calendar') function startOfMonth(date: Date): Date { - return new Date(Date.UTC(date.getFullYear(), date.getMonth(), 1)) + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1)) } -function endOfMonth(date: Date): Date { - return new Date(Date.UTC(date.getFullYear(), date.getMonth() + 1, 1)) +/** Returns the first day of the next month (exclusive upper bound for date range queries). */ +function startOfNextMonth(date: Date): Date { + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1)) } const monthLabel = computed(() => { @@ -45,7 +46,7 @@ async function fetchCalendar() { try { const from = viewDate.value.toISOString() - const to = endOfMonth(viewDate.value).toISOString() + const to = startOfNextMonth(viewDate.value).toISOString() calendarData.value = await workspaceApi.getCalendar(from, to) } catch (e: unknown) { calendarData.value = null @@ -325,15 +326,17 @@ watch(viewDate, fetchCalendar) {{ group.dateLabel }} {{ group.cards.length }} card{{ group.cards.length === 1 ? '' : 's' }}
-
- -
+ +
@@ -626,6 +630,13 @@ watch(viewDate, fetchCalendar) display: flex; flex-direction: column; gap: var(--td-space-3); + list-style: none; + padding: 0; + margin: 0; +} + +.td-timeline-card-wrapper { + display: contents; } .td-timeline-card { From 057b7b6d8f0585ab1f13694c2d85953e80ff2d40 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sun, 12 Apr 2026 01:23:19 +0100 Subject: [PATCH 18/18] Remove redundant DueDate.HasValue filter in calendar query GetByDueDateRangeAsync already filters for cards with due dates, so the additional Where clause was unnecessary. This addresses a medium-priority review comment on PR #810. --- backend/src/Taskdeck.Application/Services/WorkspaceService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/Taskdeck.Application/Services/WorkspaceService.cs b/backend/src/Taskdeck.Application/Services/WorkspaceService.cs index 2b92734aa..6e05503e6 100644 --- a/backend/src/Taskdeck.Application/Services/WorkspaceService.cs +++ b/backend/src/Taskdeck.Application/Services/WorkspaceService.cs @@ -290,7 +290,6 @@ public async Task> GetCalendarAsync( var referenceTime = DateTimeOffset.UtcNow; var calendarCards = cards - .Where(c => c.DueDate.HasValue) .Select(c => new WorkspaceCalendarCardDto( c.Id, c.BoardId,