Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Story 17.10: Inline Status Dropdown on Work Order Detail

Status: review
Status: done

## Story

Expand Down Expand Up @@ -282,3 +282,4 @@ Claude Opus 4.6

## Change Log
- 2026-03-04: All 6 tasks completed, all 2618 unit tests passing, Playwright visual verification done, story set to review
- 2026-03-05: Code review fixes merged (rollback bug, null guards, exhaustMap, test coverage), 2619 tests passing, story done
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Story 17.11: Work Order List — Primary Photo Thumbnail

Status: review
Status: done

## Story

Expand Down

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions _bmad-output/implementation-artifacts/sprint-status.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ development_status:
17-8-full-size-add-vendor-form: done # Issue #274 - M — PR #291 merged
17-9-photo-upload-multi-file-support: done # Issue #276 - M — PR #293 merged
17-10-inline-status-dropdown-wo-detail: done # Issue #277 - M — PR #294 merged
17-11-wo-list-primary-photo-thumbnail: review # Issue #270 - M
17-12-replace-year-selector-date-range: pending # Issue #279 - L
17-11-wo-list-primary-photo-thumbnail: done # Issue #270 - M — PR #295 merged
17-12-replace-year-selector-date-range: ready-for-dev # Issue #279 - L
17-13-vendor-photo-support: pending # Issue #271 - L
epic-17-retrospective: optional
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,18 @@ public DashboardController(
/// Get dashboard totals for the specified tax year (AC-4.4.1, AC-4.4.2, AC-4.4.6).
/// Returns aggregated expenses, income, net income, and property count.
/// </summary>
/// <param name="year">Tax year to aggregate totals for (required)</param>
/// <param name="year">Optional tax year to aggregate totals for (defaults to current year)</param>
/// <param name="dateFrom">Optional start date filter (YYYY-MM-DD)</param>
/// <param name="dateTo">Optional end date filter (YYYY-MM-DD)</param>
/// <returns>Dashboard totals including expenses, income, net income, and property count</returns>
/// <response code="200">Returns the dashboard totals</response>
/// <response code="401">If user is not authenticated</response>
[HttpGet("totals")]
[ProducesResponseType(typeof(DashboardTotalsDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetTotals([FromQuery] int year)
public async Task<IActionResult> GetTotals([FromQuery] int? year = null, [FromQuery] DateOnly? dateFrom = null, [FromQuery] DateOnly? dateTo = null)
{
var query = new GetDashboardTotalsQuery(year);
var query = new GetDashboardTotalsQuery(year, dateFrom, dateTo);
var response = await _mediator.Send(query);

_logger.LogInformation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,17 @@ public PropertiesController(
/// Get all properties for the current user (AC-2.1.4, AC-2.2.6).
/// </summary>
/// <param name="year">Optional tax year filter for expense/income totals</param>
/// <param name="dateFrom">Optional start date filter (YYYY-MM-DD)</param>
/// <param name="dateTo">Optional end date filter (YYYY-MM-DD)</param>
/// <returns>List of properties with summary information</returns>
/// <response code="200">Returns the list of properties</response>
/// <response code="401">If user is not authenticated</response>
[HttpGet]
[ProducesResponseType(typeof(GetAllPropertiesResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetAllProperties([FromQuery] int? year = null)
public async Task<IActionResult> GetAllProperties([FromQuery] int? year = null, [FromQuery] DateOnly? dateFrom = null, [FromQuery] DateOnly? dateTo = null)
{
var query = new GetAllPropertiesQuery(year);
var query = new GetAllPropertiesQuery(year, dateFrom, dateTo);
var response = await _mediator.Send(query);

_logger.LogInformation(
Expand All @@ -63,6 +65,8 @@ public async Task<IActionResult> GetAllProperties([FromQuery] int? year = null)
/// </summary>
/// <param name="id">Property GUID</param>
/// <param name="year">Optional tax year filter for expense totals (defaults to current year)</param>
/// <param name="dateFrom">Optional start date filter (YYYY-MM-DD)</param>
/// <param name="dateTo">Optional end date filter (YYYY-MM-DD)</param>
/// <returns>Property detail information</returns>
/// <response code="200">Returns the property detail</response>
/// <response code="401">If user is not authenticated</response>
Expand All @@ -71,9 +75,9 @@ public async Task<IActionResult> GetAllProperties([FromQuery] int? year = null)
[ProducesResponseType(typeof(PropertyDetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetPropertyById(Guid id, [FromQuery] int? year = null)
public async Task<IActionResult> GetPropertyById(Guid id, [FromQuery] int? year = null, [FromQuery] DateOnly? dateFrom = null, [FromQuery] DateOnly? dateTo = null)
{
var query = new GetPropertyByIdQuery(id, year);
var query = new GetPropertyByIdQuery(id, year, dateFrom, dateTo);
var property = await _mediator.Send(query);

if (property == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace PropertyManager.Application.Dashboard;
/// Query to get dashboard totals for the current user's account (AC-4.4.1, AC-4.4.2).
/// </summary>
/// <param name="Year">Tax year to aggregate totals for</param>
public record GetDashboardTotalsQuery(int Year) : IRequest<DashboardTotalsDto>;
public record GetDashboardTotalsQuery(int? Year = null, DateOnly? DateFrom = null, DateOnly? DateTo = null) : IRequest<DashboardTotalsDto>;

/// <summary>
/// Dashboard totals DTO containing aggregated financial data.
Expand Down Expand Up @@ -39,8 +39,9 @@ public GetDashboardTotalsQueryHandler(

public async Task<DashboardTotalsDto> Handle(GetDashboardTotalsQuery request, CancellationToken cancellationToken)
{
var yearStart = new DateOnly(request.Year, 1, 1);
var yearEnd = new DateOnly(request.Year, 12, 31);
var year = request.Year ?? DateTime.UtcNow.Year;
var yearStart = request.DateFrom ?? new DateOnly(year, 1, 1);
var yearEnd = request.DateTo ?? new DateOnly(year, 12, 31);

// Get total expenses for the year
var totalExpenses = await _dbContext.Expenses
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace PropertyManager.Application.Properties;
/// Query to get all properties for the current user's account.
/// </summary>
/// <param name="Year">Optional tax year filter for expense/income totals (defaults to current year)</param>
public record GetAllPropertiesQuery(int? Year = null) : IRequest<GetAllPropertiesResponse>;
public record GetAllPropertiesQuery(int? Year = null, DateOnly? DateFrom = null, DateOnly? DateTo = null) : IRequest<GetAllPropertiesResponse>;

/// <summary>
/// Response containing list of properties.
Expand Down Expand Up @@ -56,6 +56,8 @@ public GetAllPropertiesQueryHandler(
public async Task<GetAllPropertiesResponse> Handle(GetAllPropertiesQuery request, CancellationToken cancellationToken)
{
var year = request.Year ?? DateTime.UtcNow.Year;
var dateStart = request.DateFrom ?? new DateOnly(year, 1, 1);
var dateEnd = request.DateTo ?? new DateOnly(year, 12, 31);

// Query properties with primary photo thumbnail storage key
var propertiesData = await _dbContext.Properties
Expand All @@ -73,13 +75,13 @@ public async Task<GetAllPropertiesResponse> Handle(GetAllPropertiesQuery request
.Where(e => e.PropertyId == p.Id
&& e.AccountId == _currentUser.AccountId
&& e.DeletedAt == null
&& e.Date.Year == year)
&& e.Date >= dateStart && e.Date <= dateEnd)
.Sum(e => (decimal?)e.Amount) ?? 0m,
IncomeTotal = _dbContext.Income
.Where(i => i.PropertyId == p.Id
&& i.AccountId == _currentUser.AccountId
&& i.DeletedAt == null
&& i.Date.Year == year)
&& i.Date >= dateStart && i.Date <= dateEnd)
.Sum(i => (decimal?)i.Amount) ?? 0m,
PrimaryPhotoThumbnailStorageKey = _dbContext.PropertyPhotos
.Where(pp => pp.PropertyId == p.Id && pp.IsPrimary)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace PropertyManager.Application.Properties;
/// </summary>
/// <param name="Id">Property GUID</param>
/// <param name="Year">Optional tax year filter (defaults to current year) (AC-3.5.6)</param>
public record GetPropertyByIdQuery(Guid Id, int? Year = null) : IRequest<PropertyDetailDto?>;
public record GetPropertyByIdQuery(Guid Id, int? Year = null, DateOnly? DateFrom = null, DateOnly? DateTo = null) : IRequest<PropertyDetailDto?>;

/// <summary>
/// Detail DTO for property view page (AC-2.3.2, AC-13.3a.9).
Expand Down Expand Up @@ -74,10 +74,10 @@ public GetPropertyByIdQueryHandler(

public async Task<PropertyDetailDto?> Handle(GetPropertyByIdQuery request, CancellationToken cancellationToken)
{
// Use provided year or default to current year (AC-3.5.6)
// Use provided date range, or fall back to year-based range (AC-3.5.6)
var year = request.Year ?? DateTime.UtcNow.Year;
var yearStart = new DateOnly(year, 1, 1);
var yearEnd = new DateOnly(year, 12, 31);
var yearStart = request.DateFrom ?? new DateOnly(year, 1, 1);
var yearEnd = request.DateTo ?? new DateOnly(year, 12, 31);

var propertyData = await _dbContext.Properties
.Where(p => p.Id == request.Id && p.AccountId == _currentUser.AccountId && p.DeletedAt == null)
Expand Down
6 changes: 3 additions & 3 deletions frontend/e2e/tests/income/income-shared-components.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ test.describe('Story 16.6 — Income Page Shared Components', () => {
await expect(presetDropdown).toBeVisible();
});

test('should show 5 preset options when dropdown opened', async ({
test('should show 6 preset options when dropdown opened', async ({
page,
authenticatedUser,
}) => {
Expand All @@ -58,9 +58,9 @@ test.describe('Story 16.6 — Income Page Shared Components', () => {
// WHEN: Opening the preset dropdown
await page.locator('app-date-range-filter mat-select').click();

// THEN: All 5 preset options are available
// THEN: All 6 preset options are available (All Time, This Month, This Quarter, This Year, Last Year, Custom Range)
const options = page.locator('mat-option');
await expect(options).toHaveCount(5);
await expect(options).toHaveCount(6);
});

test('should show custom date inputs when Custom Range selected', async ({
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/app/core/components/shell/shell.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
</button>
<img src="assets/brand/logo-icon-only-white.svg" alt="Upkeep" class="header-logo">
<span class="spacer"></span>
<app-year-selector class="light-theme" />
<span class="user-name" data-testid="tablet-user-name">{{ userDisplayName }}</span>
<button
mat-icon-button
Expand All @@ -52,7 +51,6 @@
<mat-toolbar class="mobile-header" color="primary">
<img src="assets/brand/logo-icon-only-white.svg" alt="Upkeep" class="header-logo">
<span class="spacer"></span>
<app-year-selector />
<span class="user-name" data-testid="mobile-user-name">{{ userDisplayName }}</span>
<button
mat-icon-button
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/app/core/components/shell/shell.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { map } from 'rxjs';

import { SidebarNavComponent } from '../sidebar-nav/sidebar-nav.component';
import { BottomNavComponent } from '../bottom-nav/bottom-nav.component';
import { YearSelectorComponent } from '../../../shared/components/year-selector/year-selector.component';
import { MobileCaptureFabComponent } from '../../../features/receipts/components/mobile-capture-fab/mobile-capture-fab.component';
import { ReceiptSignalRService } from '../../../features/receipts/services/receipt-signalr.service';
import { SignalRService } from '../../signalr/signalr.service';
Expand Down Expand Up @@ -44,7 +43,6 @@ import { BREAKPOINTS } from '../../constants/layout.constants';
MatProgressSpinnerModule,
SidebarNavComponent,
BottomNavComponent,
YearSelectorComponent,
MobileCaptureFabComponent,
],
templateUrl: './shell.component.html',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,6 @@
<img src="assets/brand/logo-lockup-horizontal-white.svg" alt="Upkeep" class="brand-logo">
</div>

<!-- Year Selector (AC-3.5.1) -->
<div class="year-selector-container">
<app-year-selector />
</div>

<mat-divider></mat-divider>

<!-- Navigation List (AC7.1) -->
<mat-nav-list class="nav-list">
@for (item of navItems; track item.route) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,6 @@
}
}

// Year selector container (AC-3.5.1)
.year-selector-container {
display: flex;
padding: 8px 16px 16px;
// Align the calendar icon with the nav item icons (they have 8px margin + 16px padding)
padding-left: 24px;
}

// Navigation list
.nav-list {
padding: 8px 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { MatDividerModule } from '@angular/material/divider';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';

import { AuthService } from '../../services/auth.service';
import { YearSelectorComponent } from '../../../shared/components/year-selector/year-selector.component';
import { ReceiptStore } from '../../../features/receipts/stores/receipt.store';

/**
Expand Down Expand Up @@ -44,7 +43,6 @@ interface NavItem {
MatButtonModule,
MatDividerModule,
MatProgressSpinnerModule,
YearSelectorComponent,
],
templateUrl: './sidebar-nav.component.html',
styleUrl: './sidebar-nav.component.scss',
Expand Down
Loading
Loading