From f73169cab54355cf68a104478532745138000de7 Mon Sep 17 00:00:00 2001 From: aligneddev Date: Mon, 30 Mar 2026 19:16:49 +0000 Subject: [PATCH 1/3] feat: add quick ride entry options flow --- .../checklists/requirements.md | 35 +++ .../contracts/quick-ride-options-api.yaml | 81 +++++++ specs/001-quick-ride-entry/data-model.md | 94 ++++++++ specs/001-quick-ride-entry/plan.md | 90 ++++++++ specs/001-quick-ride-entry/quickstart.md | 110 ++++++++++ specs/001-quick-ride-entry/research.md | 112 ++++++++++ specs/001-quick-ride-entry/spec.md | 115 ++++++++++ specs/001-quick-ride-entry/tasks.md | 189 ++++++++++++++++ .../RidesApplicationServiceTests.cs | 146 +++++++++++++ .../Endpoints/RidesEndpointsTests.cs | 58 +++++ .../Rides/GetQuickRideOptionsService.cs | 43 ++++ src/BikeTracking.Api/BikeTracking.Api.http | 5 + .../Contracts/RidesContracts.cs | 7 + .../Endpoints/RidesEndpoints.cs | 31 +++ src/BikeTracking.Api/Program.cs | 1 + .../src/pages/RecordRidePage.test.tsx | 201 ++++++++++++++++++ .../src/pages/RecordRidePage.tsx | 53 ++++- .../src/services/ridesService.test.ts | 23 ++ .../src/services/ridesService.ts | 26 +++ .../tests/e2e/record-ride.spec.ts | 25 ++- .../tests/e2e/support/ride-helpers.ts | 7 + 21 files changed, 1448 insertions(+), 4 deletions(-) create mode 100644 specs/001-quick-ride-entry/checklists/requirements.md create mode 100644 specs/001-quick-ride-entry/contracts/quick-ride-options-api.yaml create mode 100644 specs/001-quick-ride-entry/data-model.md create mode 100644 specs/001-quick-ride-entry/plan.md create mode 100644 specs/001-quick-ride-entry/quickstart.md create mode 100644 specs/001-quick-ride-entry/research.md create mode 100644 specs/001-quick-ride-entry/spec.md create mode 100644 specs/001-quick-ride-entry/tasks.md create mode 100644 src/BikeTracking.Api/Application/Rides/GetQuickRideOptionsService.cs diff --git a/specs/001-quick-ride-entry/checklists/requirements.md b/specs/001-quick-ride-entry/checklists/requirements.md new file mode 100644 index 0000000..0c9ac60 --- /dev/null +++ b/specs/001-quick-ride-entry/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: Quick Ride Entry from Past Rides + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-03-30 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Validation pass completed on 2026-03-30; no blocking issues found. +- Spec is ready for `/speckit.plan`. \ No newline at end of file diff --git a/specs/001-quick-ride-entry/contracts/quick-ride-options-api.yaml b/specs/001-quick-ride-entry/contracts/quick-ride-options-api.yaml new file mode 100644 index 0000000..0c6bdd5 --- /dev/null +++ b/specs/001-quick-ride-entry/contracts/quick-ride-options-api.yaml @@ -0,0 +1,81 @@ +openapi: 3.0.3 +info: + title: BikeTracking Quick Ride Options API + version: 1.0.0 + description: Authenticated quick-entry options for record-ride prefill. + +paths: + /api/rides/quick-options: + get: + summary: Get up to 5 distinct quick ride options for the authenticated rider + operationId: getQuickRideOptions + responses: + '200': + description: Quick options resolved. + content: + application/json: + schema: + $ref: '#/components/schemas/QuickRideOptionsResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Unexpected retrieval failure + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +components: + schemas: + QuickRideOption: + type: object + required: + - miles + - rideMinutes + - lastUsedAtLocal + properties: + miles: + type: number + exclusiveMinimum: 0 + maximum: 200 + rideMinutes: + type: integer + minimum: 1 + lastUsedAtLocal: + type: string + format: date-time + description: Most recent local ride date/time for this miles-duration pair. + + QuickRideOptionsResponse: + type: object + required: + - options + - generatedAtUtc + properties: + options: + type: array + maxItems: 5 + items: + $ref: '#/components/schemas/QuickRideOption' + generatedAtUtc: + type: string + format: date-time + + ErrorResponse: + type: object + required: + - code + - message + properties: + code: + type: string + message: + type: string + details: + type: array + items: + type: string \ No newline at end of file diff --git a/specs/001-quick-ride-entry/data-model.md b/specs/001-quick-ride-entry/data-model.md new file mode 100644 index 0000000..26baba2 --- /dev/null +++ b/specs/001-quick-ride-entry/data-model.md @@ -0,0 +1,94 @@ +# Data Model: Quick Ride Entry from Past Rides + +**Feature**: Quick Ride Entry from Past Rides (001) +**Branch**: `001-quick-ride-entry` +**Date**: 2026-03-30 +**Phase**: Phase 1 - Design & Contracts + +## Overview + +This feature introduces a read-side model for reusable quick options derived from historical ride records. No new write-side event type is required; quick options are computed from existing rider ride data and consumed by the record-ride UI. + +## Entities + +### QuickRideOption + +Represents one distinct reusable pair of values shown to the rider for fast prefill. + +| Field | Type | Required | Validation | Notes | +|-------|------|----------|------------|-------| +| miles | number | Yes | > 0 and <= 200 | Copied into ride form miles field | +| rideMinutes | integer | Yes | > 0 | Copied into ride form duration field | +| lastUsedAtLocal | string (date-time) | Yes | valid date-time | Most recent occurrence timestamp used for ordering | + +### QuickRideOptionsResponse + +API response for quick option retrieval. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| options | array of QuickRideOption | Yes | Up to 5 rider-scoped distinct options | +| generatedAtUtc | string (date-time) | Yes | Server timestamp for response generation | + +### RideEntryFormState (Frontend) + +Client form state that receives copied values. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| miles | number or empty | Yes | User-editable required field | +| rideMinutes | integer or empty | Yes | User-editable required field for this feature's quick-copy behavior | +| isDirty | boolean | Yes | Indicates local edits after prefill/manual changes | +| selectedQuickOption | QuickRideOption or null | No | Tracks current source option for UX feedback | + +## Relationships + +- One rider can have many Ride Entries. +- Many historical Ride Entries map to one QuickRideOption when `(miles, rideMinutes)` values are identical. +- QuickRideOptionsResponse is derived from the rider's ride history and is not persisted as a standalone table requirement for this phase. + +## Derivation Rules + +1. Filter ride records to current authenticated rider. +2. Exclude records missing miles or rideMinutes. +3. Group by `(miles, rideMinutes)`. +4. For each group, keep most recent `rideDateTimeLocal` as `lastUsedAtLocal`. +5. Sort groups by `lastUsedAtLocal` descending. +6. Return top 5. + +## State Transitions + +1. Rider opens record-ride page. +2. Frontend requests `GET /api/rides/quick-options`. +3. Backend returns up to five distinct options. +4. Rider selects one option. +5. Frontend copies option values into `miles` and `rideMinutes` fields. +6. Rider may edit values. +7. Rider submits existing save flow; normal validation applies. +8. On save success, frontend refreshes quick options for future entries. + +## Validation Rules + +### API Query Layer + +- Require authenticated rider context. +- Ensure response contains at most 5 options. +- Ensure each option is unique by exact `(miles, rideMinutes)` pair. +- Ensure each returned option contains valid positive values. + +### Frontend Layer + +- Option selection must not trigger API write/save. +- Copied values remain editable. +- Existing validation messages and submission guards remain unchanged. + +### Security/Isolation + +- Query must return only options derived from the authenticated rider's rides. +- Cross-user data leakage is prohibited. + +## Failure and Empty-State Behavior + +- If options query returns empty array, quick-entry section renders empty/hidden state and manual entry remains available. +- If query fails, show non-blocking error and keep manual ride entry usable. +- If rider has fewer than 5 valid distinct patterns, return only available options. \ No newline at end of file diff --git a/specs/001-quick-ride-entry/plan.md b/specs/001-quick-ride-entry/plan.md new file mode 100644 index 0000000..bcd4aad --- /dev/null +++ b/specs/001-quick-ride-entry/plan.md @@ -0,0 +1,90 @@ +# Implementation Plan: Quick Ride Entry from Past Rides + +**Branch**: `001-quick-ride-entry` | **Date**: 2026-03-30 | **Spec**: `/specs/001-quick-ride-entry/spec.md` +**Input**: Feature specification from `/specs/001-quick-ride-entry/spec.md` + +**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow. + +## Summary + +Add a quick-entry assist to the existing record-ride flow so authenticated riders can select from up to five distinct prior miles-duration combinations and prefill form fields without auto-saving. The feature introduces a rider-scoped query contract for quick options, keeps manual editing and validation unchanged, and updates options after successful new ride submission. + +## Technical Context + +**Language/Version**: C# (.NET 10), TypeScript (React 19 + Vite), F# domain project available +**Primary Dependencies**: ASP.NET Core Minimal API, EF Core (SQLite provider), existing rides endpoints and auth/session flow, React form state patterns +**Storage**: SQLite local-file profile via EF Core; quick options derived from existing rider ride data (no new storage engine) +**Testing**: `dotnet test BikeTracking.slnx`, frontend `npm run lint`, `npm run build`, `npm run test:unit`, and `npm run test:e2e` for cross-layer flow +**Target Platform**: Local-first Aspire web app in DevContainer (browser frontend + .NET API) +**Project Type**: Web application (React frontend + Minimal API backend) +**Performance Goals**: Quick-options query should stay under constitutional API target (<500ms p95); quick option selection should prefill form immediately on client interaction +**Constraints**: Rider-scoped data isolation, no auto-save on option select, max 5 distinct options, preserve existing validation semantics, offline/error fallback to manual entry +**Scale/Scope**: One query endpoint for quick options, ride-entry UI enhancement, and tests covering duplicate suppression, limit behavior, and edit-after-prefill + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +### Pre-Research Gate Review + +| Gate | Status | Notes | +|------|--------|-------| +| DevContainer-only development | PASS | Planning commands and artifact generation executed inside the workspace container. | +| Clean architecture boundaries | PASS | Feature boundaries remain in ride-entry UI + API query service over existing ride data. | +| React + TypeScript consistency | PASS | Quick options rendered and applied through typed React state/form patterns. | +| Data validation depth | PASS | Existing client/API/DB validation retained; quick options only prefill values, not bypass rules. | +| Contract-first collaboration | PASS | Dedicated quick-options API contract defined before implementation. | +| TDD gated workflow | PASS WITH ACTION | Tasks must enforce red-first tests and explicit user confirmation of failing tests prior to coding. | +| Performance and observability expectations | PASS | Query limited to 5 distinct values and rider-scoped filter; metrics/logging remain on current pipeline. | + +No constitutional violations identified. + +### Post-Design Gate Re-Check + +| Gate | Status | Notes | +|------|--------|-------| +| Architecture and modularity | PASS | Design isolates quick-option derivation in query path and keeps create-ride command flow unchanged. | +| Contract compatibility | PASS | New endpoint is additive and does not alter existing ride history or record-ride contract behavior. | +| Validation and safety | PASS | Prefill can be edited and still must pass existing required-field/constraint validation. | +| UX consistency and accessibility | PASS | Feature remains optional, non-blocking, and compatible with existing entry form behavior. | +| Verification matrix coverage | PASS WITH ACTION | Quickstart includes required backend/frontend/e2e validation commands for cross-layer changes. | + +## Project Structure + +### Documentation (this feature) + +```text +specs/001-quick-ride-entry/ +├── plan.md +├── research.md +├── data-model.md +├── quickstart.md +├── contracts/ +│ └── quick-ride-options-api.yaml +└── tasks.md # generated by /speckit.tasks +``` + +### Source Code (repository root) +```text +src/ +├── BikeTracking.Api/ +│ ├── Endpoints/ +│ ├── Application/ +│ ├── Contracts/ +│ └── Infrastructure/ +├── BikeTracking.Api.Tests/ +│ ├── Endpoints/ +│ ├── Application/ +│ └── Infrastructure/ +└── BikeTracking.Frontend/ + ├── src/ + │ ├── pages/ + │ ├── components/ + │ └── services/ + └── tests/ +``` + +**Structure Decision**: Reuse the existing web-app split and implement a query-side quick-option slice: backend exposes rider-scoped quick options from existing rides data, frontend consumes options on record-ride load and applies selection to current form state. + +## Complexity Tracking +No constitutional violations requiring justification. diff --git a/specs/001-quick-ride-entry/quickstart.md b/specs/001-quick-ride-entry/quickstart.md new file mode 100644 index 0000000..14b8301 --- /dev/null +++ b/specs/001-quick-ride-entry/quickstart.md @@ -0,0 +1,110 @@ +# Quickstart: Quick Ride Entry from Past Rides + +**Feature**: Quick Ride Entry from Past Rides +**Branch**: `001-quick-ride-entry` +**Date**: 2026-03-30 + +## Quick Reference + +- API contract: [quick-ride-options-api.yaml](./contracts/quick-ride-options-api.yaml) +- API playground request: `src/BikeTracking.Api/BikeTracking.Api.http` (`GET /api/rides/quick-options`) +- Existing dependent contracts: + - record ride: `specs/004-create-the-record-ride-mvp/contracts/record-ride-api.yaml` + - history: `specs/005-view-history-page/contracts/ride-history-api.yaml` + +## Implementation Steps + +### 1. Add Backend Quick Options Query + +1. Add authenticated endpoint `GET /api/rides/quick-options` in API endpoint mapping. +2. Implement application query service that: +- reads rider's rides +- filters records missing miles or rideMinutes +- deduplicates by `(miles, rideMinutes)` +- orders by most recent ride datetime descending +- returns top 5 +3. Return additive DTO response matching contract. + +### 2. Integrate Frontend Record-Ride UI + +1. Add quick-entry section to existing record-ride page. +2. Fetch quick options on page load. +3. Render option chips/buttons showing miles + minutes summary. +4. On option selection, copy values into miles and rideMinutes fields. +5. Preserve ability to edit copied values before save. +6. Do not trigger save on selection. + +### 3. Refresh After Successful Save + +1. Reuse existing ride save flow. +2. After successful `POST /api/rides`, refetch quick options. +3. Keep failure mode non-blocking: if refresh fails, manual form usage continues. + +### 4. TDD Workflow (Mandatory) + +1. Write failing tests first for quick-option derivation and prefill behavior. +2. Run and capture failing outputs. +3. Obtain user confirmation that failures are meaningful. +4. Implement minimal code to pass tests. +5. Re-run tests after each meaningful change. + +## Suggested Test Coverage + +### Backend + +- returns no more than 5 options +- returns only authenticated rider options +- excludes rides missing miles or rideMinutes +- returns unique `(miles, rideMinutes)` pairs +- orders options by recency + +### Frontend + +- quick options render when returned +- selecting option copies miles and rideMinutes +- selection does not auto-submit/save +- copied values remain editable +- empty options state falls back to manual entry UX + +### E2E + +- rider with repeated patterns sees deduplicated up-to-5 options +- selecting option prefills fields, submit persists ride +- after save, quick options include new pattern when distinct + +## Verification Commands (Mandatory) + +```bash +# backend checks +cd /workspaces/neCodeBikeTracking +dotnet test BikeTracking.slnx + +# frontend checks +cd /workspaces/neCodeBikeTracking/src/BikeTracking.Frontend +npm run lint +npm run build +npm run test:unit + +# cross-layer/authenticated journey checks +npm run test:e2e +``` + +## Manual Verification Flow + +1. Start stack: `dotnet run --project src/BikeTracking.AppHost` +2. Sign in as a rider with at least 6 rides containing repeated miles-duration combinations. +3. Open record-ride page and verify no more than 5 quick options appear. +4. Select a quick option and confirm miles + duration fields are populated. +5. Edit one populated field and save ride. +6. Confirm save succeeds and no auto-save happened during selection. +7. Reopen or refresh options and verify the newest distinct pattern appears according to recency rules. + +## Acceptance Notes + +- Quick ride options are rider-scoped and derived only from rides that contain both miles and duration. +- Duplicate `(miles, rideMinutes)` combinations are collapsed so each combination appears once. +- The quick options list is ordered by most recent matching ride and capped at 5 entries. +- Selecting a quick option copies miles and duration into the form but never submits automatically. +- Riders can edit copied values before saving, and standard validation still applies. +- If quick options cannot be loaded, manual ride entry remains available without blocking the page. +- After a successful ride save, quick options are refreshed so new distinct patterns can appear immediately. \ No newline at end of file diff --git a/specs/001-quick-ride-entry/research.md b/specs/001-quick-ride-entry/research.md new file mode 100644 index 0000000..6f71b95 --- /dev/null +++ b/specs/001-quick-ride-entry/research.md @@ -0,0 +1,112 @@ +# Research: Quick Ride Entry from Past Rides + +**Feature**: Quick Ride Entry from Past Rides (001) +**Branch**: `001-quick-ride-entry` +**Date**: 2026-03-30 +**Phase**: Phase 0 - Research & Decisions + +## Research Objectives + +1. Determine where quick options should be derived (frontend from history payload vs dedicated backend query) +2. Define distinctness and ordering logic for the up-to-5 option list +3. Confirm prefill semantics so selection never bypasses validation or auto-saves +4. Define behavior when rider has little/no reusable history +5. Define refresh behavior so newly saved rides can influence future quick options + +## Key Findings + +### 1. Option Source Strategy (DECIDED) + +**Decision**: Add a dedicated rider-scoped query endpoint to return quick ride options (`GET /api/rides/quick-options`) instead of deriving from paged history responses in the frontend. + +**Rationale**: +- Avoids coupling quick-entry behavior to pagination/filter state from history views +- Keeps quick-option logic centralized and consistent across clients +- Reduces frontend data-processing complexity and payload size +- Preserves contract-first modularity with an explicit additive interface + +**Alternatives considered**: +- Reuse `/api/rides/history` and derive client-side: rejected due to pagination/filter mismatch and extra client processing +- Local cache/session-only options: rejected because they can drift from persisted rider history + +--- + +### 2. Distinctness and Ordering Rules (DECIDED) + +**Decision**: Define a quick option as a unique `(miles, rideMinutes)` pair and order options by most recently recorded occurrence, returning at most 5. + +**Rationale**: +- Matches the requirement for distinct options +- Most-recent ordering best aligns with "many rides are the same most days" +- Bounded response keeps UI fast and scan-friendly + +**Alternatives considered**: +- Use frequency ranking (most common): rejected for MVP due to extra aggregation complexity and weaker recency relevance +- Include temperature in distinctness key: rejected because requirement only asks to copy miles and duration + +--- + +### 3. Prefill Safety Semantics (DECIDED) + +**Decision**: Option selection updates only in-memory form values for miles and rideMinutes; no write occurs until explicit submit. + +**Rationale**: +- Prevents accidental saves +- Preserves existing command-side validation and user control +- Supports user edits after prefill without special exceptions + +**Alternatives considered**: +- Auto-submit on option click: rejected due to high accidental-write risk and explicit requirement conflict +- Lock copied fields from editing: rejected because riders need small adjustments on similar days + +--- + +### 4. Missing or Invalid History Handling (DECIDED) + +**Decision**: Exclude rides lacking either miles or rideMinutes from quick-option derivation and return an empty option array when no valid distinct pairs exist. + +**Rationale**: +- Ensures every option can fully populate both required quick-copy fields for this feature +- Keeps behavior deterministic and avoids partially filled prefills +- Empty response cleanly supports manual entry fallback + +**Alternatives considered**: +- Include partial records and fill only one field: rejected because feature requires copying both miles and duration +- Return placeholder/fake defaults: rejected because they are not rider-derived and could mislead users + +--- + +### 5. Option Refresh Timing (DECIDED) + +**Decision**: Refresh quick options after each successful ride save and also load options when opening the record-ride page. + +**Rationale**: +- Satisfies requirement that new rides influence future quick entry +- Keeps option list current within the active session +- Minimal complexity: same query can be reused for initial load and post-save refresh + +**Alternatives considered**: +- Refresh only on full page reload: rejected because newly repeated rides would not appear immediately +- Polling-based refresh: rejected for unnecessary complexity and network cost + +## Technical Decisions Summary + +| Decision Area | Chosen Approach | Why | +|---------------|-----------------|-----| +| Data source | Dedicated `GET /api/rides/quick-options` endpoint | Contract clarity and consistent derivation | +| Distinctness | Unique `(miles, rideMinutes)` pair | Exactly matches copy requirement | +| Ordering/limit | Most recent first, max 5 | Fast, relevant shortlist | +| Prefill behavior | Client-side copy only; explicit submit required | Prevent accidental persistence | +| Missing data handling | Exclude incomplete historical records | Avoid partial/ambiguous prefills | +| Refresh behavior | Load on page open + refresh after successful save | Keeps options current | + +## Dependencies & Assumptions + +- Existing ride recording endpoint and authenticated ride-entry page remain in place. +- Existing rides persistence already stores values needed to derive miles + duration pairs. +- Auth context is available in API to scope query results to current rider. +- No new event type is required for quick options in this feature. + +## Open Questions + +None. All planning clarifications for feature scope are resolved. \ No newline at end of file diff --git a/specs/001-quick-ride-entry/spec.md b/specs/001-quick-ride-entry/spec.md new file mode 100644 index 0000000..5baee12 --- /dev/null +++ b/specs/001-quick-ride-entry/spec.md @@ -0,0 +1,115 @@ +# Feature Specification: Quick Ride Entry from Past Rides + +**Feature Branch**: `001-quick-ride-entry` +**Created**: 2026-03-30 +**Status**: Draft +**Input**: User description: "Enable quick ride entry by allowing the user to pick from up to 5 distinct past rides. Copy in miles and duration when the user selects one. Many times my rides are the same most days." + +## User Scenarios & Testing *(mandatory)* + + + +### User Story 1 - Reuse a Frequent Ride Pattern (Priority: P1) + +As a rider entering a new ride, I want to choose from my recent distinct ride patterns so I can fill in miles and duration quickly without retyping common values. + +**Why this priority**: This is the core value requested and directly reduces repeated daily data entry. + +**Independent Test**: Can be fully tested by opening ride entry with existing ride history, selecting one quick option, and verifying miles and duration are populated in the form. + +**Acceptance Scenarios**: + +1. **Given** a rider has at least one past ride with miles and duration, **When** the rider opens the ride entry form, **Then** the rider sees up to 5 quick options representing distinct past ride patterns. +2. **Given** quick options are shown, **When** the rider selects one option, **Then** the miles and duration fields in the ride entry form are populated with that option's values. +3. **Given** a rider selects a quick option, **When** values are copied into the form, **Then** no ride is saved until the rider explicitly submits the form. + +--- + +### User Story 2 - Keep Flexibility After Prefill (Priority: P2) + +As a rider, I want to adjust copied miles or duration before saving so I can handle days that are similar but not identical. + +**Why this priority**: Riders often repeat routes with small variations; editing after prefill preserves speed and accuracy. + +**Independent Test**: Can be fully tested by selecting a quick option, editing one copied value, and successfully saving with the edited values. + +**Acceptance Scenarios**: + +1. **Given** miles and duration were copied from a quick option, **When** the rider edits either field, **Then** the rider can save the ride with the updated values. +2. **Given** copied values are present, **When** the rider clears one required field, **Then** the form prevents submission and shows the same validation behavior as manual entry. + +--- + +### User Story 3 - See Useful, Non-Duplicate Suggestions (Priority: P3) + +As a rider, I want the quick option list to avoid duplicate entries and stay short so I can choose quickly without scanning clutter. + +**Why this priority**: A concise, distinct list preserves the speed benefit and avoids confusion. + +**Independent Test**: Can be fully tested by creating ride history with repeated and unique patterns, then verifying the quick options contain no duplicates and never exceed five entries. + +**Acceptance Scenarios**: + +1. **Given** ride history includes repeated rides with identical miles and duration, **When** quick options are generated, **Then** each miles-duration combination appears at most once. +2. **Given** ride history contains more than five distinct miles-duration combinations, **When** quick options are shown, **Then** only five options are displayed. +3. **Given** ride history contains fewer than five distinct combinations, **When** quick options are shown, **Then** only the available distinct options are displayed. + +--- + +### Edge Cases + +- Rider has no prior rides: no quick options are shown and manual entry remains fully available. +- Rider has prior rides but some are incomplete or invalid for quick reuse: those entries are excluded from quick options. +- Rider has many repeated rides with the same miles and duration: only one option appears for that combination. +- Rider selects a quick option, then changes their mind: they can edit copied values or choose another quick option before saving. +- Rider opens quick options while not authenticated: quick options are not shown. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST display a quick entry section in the ride entry experience for authenticated riders. +- **FR-002**: System MUST present at most 5 quick options derived from the rider's own past rides. +- **FR-003**: System MUST define distinct quick options by unique miles-and-duration combinations. +- **FR-004**: System MUST copy miles and duration from the selected quick option into the ride entry form fields. +- **FR-005**: System MUST NOT automatically save a ride when a quick option is selected. +- **FR-006**: System MUST allow riders to edit copied miles and duration before submitting. +- **FR-007**: System MUST preserve existing required-field validation behavior after quick option selection. +- **FR-008**: System MUST exclude past rides that do not contain both miles and duration from quick options. +- **FR-009**: System MUST show only available distinct quick options when fewer than 5 exist. +- **FR-010**: System MUST ensure quick options are scoped to the currently authenticated rider. +- **FR-011**: System MUST refresh the set of quick options after a new ride is successfully saved so future entries can reuse it. +- **FR-012**: System MUST keep manual ride entry fully functional when no quick options are available. + +### Key Entities *(include if feature involves data)* + +- **Ride Entry**: A rider-owned record containing at least miles and duration values for a completed ride. +- **Quick Ride Option**: A reusable suggestion representing one distinct miles-and-duration combination derived from a rider's past rides. +- **Quick Option Set**: The ordered collection of up to five Quick Ride Options presented to a rider during ride entry. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: At least 85% of rides started from the entry screen can be completed in under 15 seconds when a rider uses a quick option. +- **SC-002**: At least 90% of riders with repeated ride patterns use a quick option at least once within their first 10 new rides after release. +- **SC-003**: 100% of quick option selections populate both miles and duration fields with the selected values. +- **SC-004**: Ride-entry abandonment rate for riders with at least 5 prior rides decreases by at least 20% compared to pre-release baseline. + +## Assumptions + +- The ride entry experience already exists and supports manual miles and duration input. +- Miles and duration are required fields for a valid ride entry. +- When more than 5 distinct patterns exist, the system presents the most recently used distinct combinations first. +- Quick options are informational shortcuts and do not replace existing permissions or validation rules. diff --git a/specs/001-quick-ride-entry/tasks.md b/specs/001-quick-ride-entry/tasks.md new file mode 100644 index 0000000..156d9ff --- /dev/null +++ b/specs/001-quick-ride-entry/tasks.md @@ -0,0 +1,189 @@ +# Tasks: Quick Ride Entry from Past Rides + +**Input**: Design documents from `/specs/001-quick-ride-entry/` +**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/quick-ride-options-api.yaml + +**Tests**: Tests are included because this feature plan requires a TDD-first workflow. + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Align feature docs and local API playground artifacts before implementation. + +- [X] T001 Update API playground examples for quick options in src/BikeTracking.Api/BikeTracking.Api.http +- [X] T002 Sync quick options contract notes in specs/001-quick-ride-entry/quickstart.md + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Add core contracts and service wiring required by all user stories. + +**⚠️ CRITICAL**: No user story work starts until this phase is complete. + +- [X] T003 Add QuickRideOption and QuickRideOptionsResponse records in src/BikeTracking.Api/Contracts/RidesContracts.cs +- [X] T004 Create query service scaffold in src/BikeTracking.Api/Application/Rides/GetQuickRideOptionsService.cs +- [X] T005 Wire GetQuickRideOptionsService registration in src/BikeTracking.Api/Program.cs +- [X] T006 Add GET /api/rides/quick-options endpoint mapping in src/BikeTracking.Api/Endpoints/RidesEndpoints.cs +- [X] T007 Add frontend quick-options DTOs and API client method in src/BikeTracking.Frontend/src/services/ridesService.ts + +**Checkpoint**: Foundation complete; user-story work can proceed. + +--- + +## Phase 3: User Story 1 - Reuse a Frequent Ride Pattern (Priority: P1) 🎯 MVP + +**Goal**: Let riders view quick options and prefill miles/duration from one click without auto-saving. + +**Independent Test**: Open record ride, select a quick option, verify miles and duration are populated and no save occurs until submit. + +### Tests for User Story 1 + +- [X] T008 [P] [US1] Add failing backend service tests for authenticated rider-scoped quick options retrieval in src/BikeTracking.Api.Tests/Application/RidesApplicationServiceTests.cs +- [X] T009 [P] [US1] Add failing endpoint tests for GET /api/rides/quick-options success and 401 behavior in src/BikeTracking.Api.Tests/Endpoints/RidesEndpointsTests.cs +- [X] T010 [P] [US1] Add failing frontend service tests for getQuickRideOptions request/response handling in src/BikeTracking.Frontend/src/services/ridesService.test.ts +- [X] T011 [P] [US1] Add failing page tests for rendering quick options and prefilling fields on selection in src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx +- [X] T012 [US1] Run failing US1 test set and capture red results for approval via src/BikeTracking.Api.Tests/Application/RidesApplicationServiceTests.cs and src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx + +### Implementation for User Story 1 + +- [X] T013 [US1] Implement rider-scoped quick options query logic in src/BikeTracking.Api/Application/Rides/GetQuickRideOptionsService.cs +- [X] T014 [US1] Implement GET /api/rides/quick-options handler and response mapping in src/BikeTracking.Api/Endpoints/RidesEndpoints.cs +- [X] T015 [US1] Implement getQuickRideOptions API method in src/BikeTracking.Frontend/src/services/ridesService.ts +- [X] T016 [US1] Integrate quick options UI rendering and prefill selection behavior in src/BikeTracking.Frontend/src/pages/RecordRidePage.tsx +- [X] T017 [US1] Run US1 backend/frontend tests to reach green in src/BikeTracking.Api.Tests/Endpoints/RidesEndpointsTests.cs and src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx + +**Checkpoint**: User Story 1 is independently functional and demoable. + +--- + +## Phase 4: User Story 2 - Keep Flexibility After Prefill (Priority: P2) + +**Goal**: Preserve user control to edit copied values and keep existing validation/submission behavior. + +**Independent Test**: Select quick option, edit copied value, submit successfully; clear a required field and observe standard validation blocking. + +### Tests for User Story 2 + +- [X] T018 [P] [US2] Add failing page tests proving copied values remain editable and validation still blocks invalid submit in src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx +- [X] T019 [P] [US2] Add failing e2e test for select-edit-save flow in src/BikeTracking.Frontend/tests/e2e/record-ride.spec.ts +- [X] T020 [US2] Run failing US2 tests and capture red results for approval via src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx and src/BikeTracking.Frontend/tests/e2e/record-ride.spec.ts + +### Implementation for User Story 2 + +- [X] T021 [US2] Ensure quick-option selection updates form state without auto-submit in src/BikeTracking.Frontend/src/pages/RecordRidePage.tsx +- [X] T022 [US2] Preserve and validate editable copied fields in existing submit path in src/BikeTracking.Frontend/src/pages/RecordRidePage.tsx +- [X] T023 [US2] Run US2 unit and e2e tests to green in src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx and src/BikeTracking.Frontend/tests/e2e/record-ride.spec.ts + +**Checkpoint**: User Story 2 works independently with existing validation semantics. + +--- + +## Phase 5: User Story 3 - See Useful, Non-Duplicate Suggestions (Priority: P3) + +**Goal**: Ensure quick options are distinct, capped at 5, recency-ordered, and resilient for empty/error cases. + +**Independent Test**: Seed repeated and unique rides, verify no duplicate options, cap of 5, and graceful empty/failure fallback. + +### Tests for User Story 3 + +- [X] T024 [P] [US3] Add failing backend tests for distinct pair deduplication, recency ordering, and max-5 limit in src/BikeTracking.Api.Tests/Application/RidesApplicationServiceTests.cs +- [X] T025 [P] [US3] Add failing endpoint tests for excluding incomplete rides and returning empty options array in src/BikeTracking.Api.Tests/Endpoints/RidesEndpointsTests.cs +- [X] T026 [P] [US3] Add failing frontend tests for empty and fetch-failure quick options fallback states in src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx +- [X] T027 [US3] Run failing US3 tests and capture red results for approval via src/BikeTracking.Api.Tests/Application/RidesApplicationServiceTests.cs and src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx + +### Implementation for User Story 3 + +- [X] T028 [US3] Implement strict distinct `(miles, rideMinutes)` grouping, recency ordering, and top-5 limit in src/BikeTracking.Api/Application/Rides/GetQuickRideOptionsService.cs +- [X] T029 [US3] Exclude incomplete records and return additive empty response shape in src/BikeTracking.Api/Application/Rides/GetQuickRideOptionsService.cs +- [X] T030 [US3] Implement frontend empty/error fallback rendering for quick-entry section in src/BikeTracking.Frontend/src/pages/RecordRidePage.tsx +- [X] T031 [US3] Refresh quick options after successful ride save in src/BikeTracking.Frontend/src/pages/RecordRidePage.tsx +- [X] T032 [US3] Run US3 backend/frontend tests to green in src/BikeTracking.Api.Tests/Endpoints/RidesEndpointsTests.cs and src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx + +**Checkpoint**: User Story 3 is independently functional with robust option quality behavior. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Final consistency, docs, and full verification across all stories. + +- [X] T033 [P] Add quick options request helper coverage updates in src/BikeTracking.Frontend/tests/e2e/support/ride-helpers.ts +- [X] T034 [P] Update feature docs and acceptance notes in specs/001-quick-ride-entry/quickstart.md +- [X] T035 Run full verification matrix commands from specs/001-quick-ride-entry/quickstart.md and record results in specs/001-quick-ride-entry/tasks.md + +### Verification Results + +- 2026-03-30: `dotnet test BikeTracking.slnx` executed from repo root. Result: failed due to 5 pre-existing backend failures in `BikeTracking.Api.Tests.Endpoints.Rides.DeleteRideEndpointTests`, including `DeleteRide_AlreadyDeleted_ReturnsIdempotent200Ok` at `src/BikeTracking.Api.Tests/Endpoints/Rides/DeleteRideEndpointTests.cs:129` during host startup. +- 2026-03-30: `cd src/BikeTracking.Frontend && npm run lint && npm run build && npm run test:unit && npm run test:e2e` executed successfully. +- 2026-03-30: Frontend verification summary: 19 Playwright E2E tests passed; record-ride quick-entry scenarios passed, including quick-option edit-before-save. + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- Setup (Phase 1) has no dependencies. +- Foundational (Phase 2) depends on Setup and blocks all user stories. +- User Story phases (Phase 3-5) depend on Foundational completion. +- Polish (Phase 6) depends on completion of the selected user stories. + +### User Story Dependencies + +- **US1 (P1)** starts immediately after Phase 2 and delivers MVP. +- **US2 (P2)** depends on US1 UI prefill behavior being present. +- **US3 (P3)** depends on US1 query/endpoint baseline and refines result quality and fallback behavior. + +### Within Each User Story + +- Write tests first and run them red before implementation. +- Implement service/endpoint/frontend behavior after red tests are approved. +- Run story-specific tests to green before moving on. + +## Parallel Opportunities + +- Phase 2: T003-T007 can be split by file ownership after agreeing contracts. +- US1: T008-T011 are parallel test authoring tasks. +- US2: T018 and T019 can run in parallel. +- US3: T024-T026 are parallel test authoring tasks. +- Polish: T033 and T034 can run in parallel. + +## Parallel Example: User Story 1 + +```bash +# Parallel test authoring tasks: +T008 src/BikeTracking.Api.Tests/Application/RidesApplicationServiceTests.cs +T009 src/BikeTracking.Api.Tests/Endpoints/RidesEndpointsTests.cs +T010 src/BikeTracking.Frontend/src/services/ridesService.test.ts +T011 src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx +``` + +## Parallel Example: User Story 3 + +```bash +# Parallel test authoring tasks: +T024 src/BikeTracking.Api.Tests/Application/RidesApplicationServiceTests.cs +T025 src/BikeTracking.Api.Tests/Endpoints/RidesEndpointsTests.cs +T026 src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx +``` + +## Implementation Strategy + +### MVP First (US1) + +1. Complete Phase 1 and Phase 2. +2. Deliver US1 (quick option retrieval + prefill without auto-save). +3. Validate US1 independently before continuing. + +### Incremental Delivery + +1. Add US2 to preserve editable copied values and validation behavior. +2. Add US3 to enforce distinct/limit/recency quality and fallback states. +3. Finish with Phase 6 full verification. + +### Team Parallelization + +1. One developer owns backend query/endpoint tasks. +2. One developer owns frontend page/service tasks. +3. One developer owns tests/e2e tasks. +4. Merge per story checkpoint after green tests. \ No newline at end of file diff --git a/src/BikeTracking.Api.Tests/Application/RidesApplicationServiceTests.cs b/src/BikeTracking.Api.Tests/Application/RidesApplicationServiceTests.cs index 6cac04e..0bb9731 100644 --- a/src/BikeTracking.Api.Tests/Application/RidesApplicationServiceTests.cs +++ b/src/BikeTracking.Api.Tests/Application/RidesApplicationServiceTests.cs @@ -167,6 +167,152 @@ public async Task GetRideDefaultsService_ReturnsLastRideDefaults() Assert.Equal(72m, defaults.DefaultTemperature); } + [Fact] + public async Task GetQuickRideOptionsService_ReturnsOnlyAuthenticatedRiderOptions() + { + using var context = CreateDbContext(); + var riderA = new UserEntity + { + DisplayName = "Quick Rider A", + NormalizedName = "quick rider a", + CreatedAtUtc = DateTime.UtcNow, + }; + var riderB = new UserEntity + { + DisplayName = "Quick Rider B", + NormalizedName = "quick rider b", + CreatedAtUtc = DateTime.UtcNow, + }; + context.Users.AddRange(riderA, riderB); + await context.SaveChangesAsync(); + + context.Rides.AddRange( + new RideEntity + { + RiderId = riderA.UserId, + RideDateTimeLocal = DateTime.Now.AddDays(-1), + Miles = 8m, + RideMinutes = 30, + CreatedAtUtc = DateTime.UtcNow, + }, + new RideEntity + { + RiderId = riderA.UserId, + RideDateTimeLocal = DateTime.Now, + Miles = 9m, + RideMinutes = 35, + CreatedAtUtc = DateTime.UtcNow, + }, + new RideEntity + { + RiderId = riderB.UserId, + RideDateTimeLocal = DateTime.Now, + Miles = 50m, + RideMinutes = 120, + CreatedAtUtc = DateTime.UtcNow, + } + ); + await context.SaveChangesAsync(); + + var service = new GetQuickRideOptionsService(context); + var response = await service.ExecuteAsync(riderA.UserId); + + Assert.NotNull(response); + foreach (var option in response.Options) + { + Assert.True(option.Miles < 20m); + Assert.True(option.RideMinutes < 60); + } + } + + [Fact] + public async Task GetQuickRideOptionsService_DeduplicatesByMilesAndRideMinutes_AndOrdersByMostRecent() + { + using var context = CreateDbContext(); + var user = new UserEntity + { + DisplayName = "Quick Rider Distinct", + NormalizedName = "quick rider distinct", + CreatedAtUtc = DateTime.UtcNow, + }; + context.Users.Add(user); + await context.SaveChangesAsync(); + + var now = DateTime.Now; + context.Rides.AddRange( + new RideEntity + { + RiderId = user.UserId, + RideDateTimeLocal = now.AddDays(-3), + Miles = 10m, + RideMinutes = 30, + CreatedAtUtc = DateTime.UtcNow, + }, + new RideEntity + { + RiderId = user.UserId, + RideDateTimeLocal = now.AddDays(-1), + Miles = 10m, + RideMinutes = 30, + CreatedAtUtc = DateTime.UtcNow, + }, + new RideEntity + { + RiderId = user.UserId, + RideDateTimeLocal = now, + Miles = 8m, + RideMinutes = 25, + CreatedAtUtc = DateTime.UtcNow, + } + ); + await context.SaveChangesAsync(); + + var service = new GetQuickRideOptionsService(context); + var response = await service.ExecuteAsync(user.UserId); + + Assert.Equal(2, response.Options.Count); + Assert.Equal(8m, response.Options[0].Miles); + Assert.Equal(25, response.Options[0].RideMinutes); + Assert.Equal(10m, response.Options[1].Miles); + Assert.Equal(30, response.Options[1].RideMinutes); + } + + [Fact] + public async Task GetQuickRideOptionsService_ReturnsAtMostFiveDistinctOptions() + { + using var context = CreateDbContext(); + var user = new UserEntity + { + DisplayName = "Quick Rider Limit", + NormalizedName = "quick rider limit", + CreatedAtUtc = DateTime.UtcNow, + }; + context.Users.Add(user); + await context.SaveChangesAsync(); + + var now = DateTime.Now; + for (var index = 0; index < 6; index++) + { + context.Rides.Add( + new RideEntity + { + RiderId = user.UserId, + RideDateTimeLocal = now.AddDays(-index), + Miles = 5m + index, + RideMinutes = 20 + index, + CreatedAtUtc = DateTime.UtcNow, + } + ); + } + + await context.SaveChangesAsync(); + + var service = new GetQuickRideOptionsService(context); + var response = await service.ExecuteAsync(user.UserId); + + Assert.Equal(5, response.Options.Count); + } + // History service tests [Fact] diff --git a/src/BikeTracking.Api.Tests/Endpoints/RidesEndpointsTests.cs b/src/BikeTracking.Api.Tests/Endpoints/RidesEndpointsTests.cs index a321d1e..85b699d 100644 --- a/src/BikeTracking.Api.Tests/Endpoints/RidesEndpointsTests.cs +++ b/src/BikeTracking.Api.Tests/Endpoints/RidesEndpointsTests.cs @@ -153,6 +153,63 @@ public async Task GetRideDefaults_WithoutAuth_Returns401() Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } + [Fact] + public async Task GetQuickRideOptions_WithAuth_Returns200() + { + await using var host = await RecordRideApiHost.StartAsync(); + var userId = await host.SeedUserAsync("QuickOptionsUser"); + await host.RecordRideAsync(userId, miles: 11.5m, rideMinutes: 39, temperature: 65m); + + var response = await host.Client.GetWithAuthAsync("/api/rides/quick-options", userId); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var payload = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(payload); + Assert.NotNull(payload.Options); + Assert.NotEmpty(payload.Options); + } + + [Fact] + public async Task GetQuickRideOptions_WithoutAuth_Returns401() + { + await using var host = await RecordRideApiHost.StartAsync(); + + var response = await host.Client.GetAsync("/api/rides/quick-options"); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task GetQuickRideOptions_ExcludesRidesWithoutRideMinutes() + { + await using var host = await RecordRideApiHost.StartAsync(); + var userId = await host.SeedUserAsync("QuickOptionsIncomplete"); + await host.RecordRideAsync(userId, miles: 11.5m, rideMinutes: 39, temperature: 65m); + await host.RecordRideAsync(userId, miles: 7.25m, rideMinutes: null, temperature: 55m); + + var response = await host.Client.GetWithAuthAsync("/api/rides/quick-options", userId); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var payload = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(payload); + Assert.All(payload.Options, option => Assert.True(option.RideMinutes > 0)); + Assert.DoesNotContain(payload.Options, option => option.Miles == 7.25m); + } + + [Fact] + public async Task GetQuickRideOptions_WithoutEligibleRides_ReturnsEmptyOptionsArray() + { + await using var host = await RecordRideApiHost.StartAsync(); + var userId = await host.SeedUserAsync("QuickOptionsEmpty"); + + var response = await host.Client.GetWithAuthAsync("/api/rides/quick-options", userId); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var payload = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(payload); + Assert.Empty(payload.Options); + } + // History endpoint tests [Fact] @@ -423,6 +480,7 @@ public static async Task StartAsync() // Add Rides services builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/BikeTracking.Api/Application/Rides/GetQuickRideOptionsService.cs b/src/BikeTracking.Api/Application/Rides/GetQuickRideOptionsService.cs new file mode 100644 index 0000000..5ef83f2 --- /dev/null +++ b/src/BikeTracking.Api/Application/Rides/GetQuickRideOptionsService.cs @@ -0,0 +1,43 @@ +using BikeTracking.Api.Contracts; +using BikeTracking.Api.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace BikeTracking.Api.Application.Rides; + +public sealed class GetQuickRideOptionsService(BikeTrackingDbContext dbContext) +{ + /// + /// Returns quick ride options for the authenticated rider. + /// Full deduplication/ordering rules are implemented in user-story phases. + /// + public async Task ExecuteAsync( + long riderId, + CancellationToken cancellationToken = default + ) + { + var rides = await dbContext + .Rides.Where(r => r.RiderId == riderId && r.RideMinutes.HasValue) + .AsNoTracking() + .Select(r => new + { + r.Miles, + RideMinutes = r.RideMinutes!.Value, + r.RideDateTimeLocal, + }) + .ToListAsync(cancellationToken); + + var options = rides + .GroupBy(ride => new { ride.Miles, ride.RideMinutes }) + .Select(group => new QuickRideOption( + group.Key.Miles, + group.Key.RideMinutes, + group.Max(ride => ride.RideDateTimeLocal) + )) + .OrderByDescending(option => option.LastUsedAtLocal) + .AsNoTracking() + .Take(5) + .ToList(); + + return new QuickRideOptionsResponse(options, DateTime.UtcNow); + } +} diff --git a/src/BikeTracking.Api/BikeTracking.Api.http b/src/BikeTracking.Api/BikeTracking.Api.http index a57460b..b6192a6 100644 --- a/src/BikeTracking.Api/BikeTracking.Api.http +++ b/src/BikeTracking.Api/BikeTracking.Api.http @@ -22,6 +22,11 @@ GET {{ApiService_HostAddress}}/api/rides/defaults Accept: application/json X-User-Id: {{RiderId}} +### Get quick ride options +GET {{ApiService_HostAddress}}/api/rides/quick-options +Accept: application/json +X-User-Id: {{RiderId}} + ### Get unfiltered ride history GET {{ApiService_HostAddress}}/api/rides/history?page=1&pageSize=25 Accept: application/json diff --git a/src/BikeTracking.Api/Contracts/RidesContracts.cs b/src/BikeTracking.Api/Contracts/RidesContracts.cs index 592b62c..18f5911 100644 --- a/src/BikeTracking.Api/Contracts/RidesContracts.cs +++ b/src/BikeTracking.Api/Contracts/RidesContracts.cs @@ -31,6 +31,13 @@ public sealed record RideDefaultsResponse( decimal? DefaultTemperature = null ); +public sealed record QuickRideOption(decimal Miles, int RideMinutes, DateTime LastUsedAtLocal); + +public sealed record QuickRideOptionsResponse( + IReadOnlyList Options, + DateTime GeneratedAtUtc +); + public sealed record EditRideRequest( [property: Required(ErrorMessage = "Ride date/time is required")] DateTime RideDateTimeLocal, [property: Required(ErrorMessage = "Miles is required")] diff --git a/src/BikeTracking.Api/Endpoints/RidesEndpoints.cs b/src/BikeTracking.Api/Endpoints/RidesEndpoints.cs index 3319bcf..29932a0 100644 --- a/src/BikeTracking.Api/Endpoints/RidesEndpoints.cs +++ b/src/BikeTracking.Api/Endpoints/RidesEndpoints.cs @@ -27,6 +27,14 @@ public static IEndpointRouteBuilder MapRidesEndpoints(this IEndpointRouteBuilder .Produces(StatusCodes.Status401Unauthorized) .RequireAuthorization(); + group + .MapGet("/quick-options", GetQuickRideOptions) + .WithName("GetQuickRideOptions") + .WithSummary("Get quick ride options for the authenticated rider") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .RequireAuthorization(); + group .MapGet("/history", GetRideHistory) .WithName("GetRideHistory") @@ -175,6 +183,29 @@ private static async Task GetRideHistory( } } + private static async Task GetQuickRideOptions( + HttpContext context, + GetQuickRideOptionsService quickRideOptionsService, + CancellationToken cancellationToken + ) + { + try + { + var userIdString = context.User.FindFirst("sub")?.Value; + if (!long.TryParse(userIdString, out var riderId) || riderId <= 0) + return Results.Unauthorized(); + + var response = await quickRideOptionsService.ExecuteAsync(riderId, cancellationToken); + return Results.Ok(response); + } + catch + { + return Results.BadRequest( + new ErrorResponse("ERROR", "An error occurred while retrieving quick ride options") + ); + } + } + private static async Task PutEditRide( [FromRoute] long rideId, [FromBody] EditRideRequest request, diff --git a/src/BikeTracking.Api/Program.cs b/src/BikeTracking.Api/Program.cs index a382452..2a27407 100644 --- a/src/BikeTracking.Api/Program.cs +++ b/src/BikeTracking.Api/Program.cs @@ -36,6 +36,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx b/src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx index f7375e8..8cec693 100644 --- a/src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx +++ b/src/BikeTracking.Frontend/src/pages/RecordRidePage.test.tsx @@ -6,17 +6,23 @@ import { RecordRidePage } from '../pages/RecordRidePage' // Mock the ridesService vi.mock('../services/ridesService', () => ({ getRideDefaults: vi.fn(), + getQuickRideOptions: vi.fn(), recordRide: vi.fn(), })) import * as ridesService from '../services/ridesService' const mockGetRideDefaults = vi.mocked(ridesService.getRideDefaults) +const mockGetQuickRideOptions = vi.mocked(ridesService.getQuickRideOptions) const mockRecordRide = vi.mocked(ridesService.recordRide) describe('RecordRidePage', () => { beforeEach(() => { vi.clearAllMocks() + mockGetQuickRideOptions.mockResolvedValue({ + options: [], + generatedAtUtc: new Date().toISOString(), + }) }) it('should render form fields', async () => { @@ -200,4 +206,199 @@ describe('RecordRidePage', () => { expect(screen.getByText(/server error/i)).toBeInTheDocument() }) }) + + it('should render quick ride options when available', async () => { + mockGetRideDefaults.mockResolvedValue({ + hasPreviousRide: false, + defaultRideDateTimeLocal: new Date().toISOString(), + }) + mockGetQuickRideOptions.mockResolvedValue({ + options: [ + { + miles: 10.5, + rideMinutes: 40, + lastUsedAtLocal: new Date().toISOString(), + }, + ], + generatedAtUtc: new Date().toISOString(), + }) + + render( + + + + ) + + await waitFor(() => { + expect(screen.getByRole('button', { name: /10\.5 mi .* 40 min/i })).toBeInTheDocument() + }) + }) + + it('should prefill miles and duration when quick option selected', async () => { + mockGetRideDefaults.mockResolvedValue({ + hasPreviousRide: false, + defaultRideDateTimeLocal: new Date().toISOString(), + }) + mockGetQuickRideOptions.mockResolvedValue({ + options: [ + { + miles: 9.25, + rideMinutes: 33, + lastUsedAtLocal: new Date().toISOString(), + }, + ], + generatedAtUtc: new Date().toISOString(), + }) + + render( + + + + ) + + const optionButton = await screen.findByRole('button', { + name: /9\.25 mi .* 33 min/i, + }) + fireEvent.click(optionButton) + + const milesInput = screen.getByLabelText(/miles/i) as HTMLInputElement + const minutesInput = screen.getByLabelText(/duration/i) as HTMLInputElement + + expect(milesInput.value).toBe('9.25') + expect(minutesInput.value).toBe('33') + expect(mockRecordRide).not.toHaveBeenCalled() + }) + + it('should allow editing copied values and submit edited payload', async () => { + mockGetRideDefaults.mockResolvedValue({ + hasPreviousRide: false, + defaultRideDateTimeLocal: new Date().toISOString(), + }) + mockGetQuickRideOptions.mockResolvedValue({ + options: [ + { + miles: 7.5, + rideMinutes: 25, + lastUsedAtLocal: new Date().toISOString(), + }, + ], + generatedAtUtc: new Date().toISOString(), + }) + mockRecordRide.mockResolvedValue({ + rideId: 222, + riderId: 1, + savedAtUtc: new Date().toISOString(), + eventStatus: 'Queued', + }) + + render( + + + + ) + + const optionButton = await screen.findByRole('button', { + name: /7\.5 mi .* 25 min/i, + }) + fireEvent.click(optionButton) + + const milesInput = screen.getByLabelText(/miles/i) as HTMLInputElement + const minutesInput = screen.getByLabelText(/duration/i) as HTMLInputElement + + fireEvent.change(milesInput, { target: { value: '8.25' } }) + fireEvent.change(minutesInput, { target: { value: '29' } }) + + const submitButton = screen.getByRole('button', { name: /record ride/i }) + fireEvent.click(submitButton) + + await waitFor(() => { + expect(mockRecordRide).toHaveBeenCalledWith( + expect.objectContaining({ + miles: 8.25, + rideMinutes: 29, + }) + ) + }) + }) + + it('should block submit when copied miles is cleared', async () => { + mockGetRideDefaults.mockResolvedValue({ + hasPreviousRide: false, + defaultRideDateTimeLocal: new Date().toISOString(), + }) + mockGetQuickRideOptions.mockResolvedValue({ + options: [ + { + miles: 6.4, + rideMinutes: 22, + lastUsedAtLocal: new Date().toISOString(), + }, + ], + generatedAtUtc: new Date().toISOString(), + }) + + render( + + + + ) + + const optionButton = await screen.findByRole('button', { + name: /6\.4 mi .* 22 min/i, + }) + fireEvent.click(optionButton) + + const milesInput = screen.getByLabelText(/miles/i) as HTMLInputElement + fireEvent.change(milesInput, { target: { value: '' } }) + + const submitButton = screen.getByRole('button', { name: /record ride/i }) + fireEvent.click(submitButton) + + await waitFor(() => { + expect(mockRecordRide).not.toHaveBeenCalled() + expect(screen.getByText(/miles must be greater than 0/i)).toBeInTheDocument() + }) + }) + + it('should not render quick ride options section when no options exist', async () => { + mockGetRideDefaults.mockResolvedValue({ + hasPreviousRide: false, + defaultRideDateTimeLocal: new Date().toISOString(), + }) + mockGetQuickRideOptions.mockResolvedValue({ + options: [], + generatedAtUtc: new Date().toISOString(), + }) + + render( + + + + ) + + await waitFor(() => { + expect(screen.getByLabelText(/miles/i)).toBeInTheDocument() + expect(screen.queryByRole('heading', { name: /quick ride options/i })).not.toBeInTheDocument() + }) + }) + + it('should keep manual entry available when quick options fetch fails', async () => { + mockGetRideDefaults.mockResolvedValue({ + hasPreviousRide: false, + defaultRideDateTimeLocal: new Date().toISOString(), + }) + mockGetQuickRideOptions.mockRejectedValue(new Error('Quick options unavailable')) + + render( + + + + ) + + await waitFor(() => { + expect(screen.getByLabelText(/miles/i)).toBeInTheDocument() + expect(screen.getByRole('button', { name: /record ride/i })).toBeInTheDocument() + expect(screen.queryByRole('heading', { name: /quick ride options/i })).not.toBeInTheDocument() + }) + }) }) diff --git a/src/BikeTracking.Frontend/src/pages/RecordRidePage.tsx b/src/BikeTracking.Frontend/src/pages/RecordRidePage.tsx index 09dc06d..b18c623 100644 --- a/src/BikeTracking.Frontend/src/pages/RecordRidePage.tsx +++ b/src/BikeTracking.Frontend/src/pages/RecordRidePage.tsx @@ -1,18 +1,29 @@ import { useEffect, useState } from 'react' -import type { RecordRideRequest } from '../services/ridesService' -import { recordRide, getRideDefaults } from '../services/ridesService' +import type { QuickRideOption, RecordRideRequest } from '../services/ridesService' +import { getQuickRideOptions, recordRide, getRideDefaults } from '../services/ridesService' export function RecordRidePage() { const [rideDateTimeLocal, setRideDateTimeLocal] = useState('') const [miles, setMiles] = useState('') const [rideMinutes, setRideMinutes] = useState('') const [temperature, setTemperature] = useState('') + const [quickRideOptions, setQuickRideOptions] = useState([]) const [loading, setLoading] = useState(true) const [submitting, setSubmitting] = useState(false) const [successMessage, setSuccessMessage] = useState('') const [errorMessage, setErrorMessage] = useState('') + const loadQuickRideOptions = async () => { + try { + const quickOptionsResponse = await getQuickRideOptions() + setQuickRideOptions(quickOptionsResponse.options) + } catch (error) { + setQuickRideOptions([]) + console.error('Failed to load quick ride options:', error) + } + } + useEffect(() => { const initializeDefaults = async () => { try { @@ -33,6 +44,10 @@ export function RecordRidePage() { } } catch (error) { console.error('Failed to load defaults:', error) + } + + try { + await loadQuickRideOptions() } finally { setLoading(false) } @@ -41,6 +56,11 @@ export function RecordRidePage() { initializeDefaults() }, []) + const applyQuickRideOption = (option: QuickRideOption) => { + setMiles(option.miles.toString()) + setRideMinutes(option.rideMinutes.toString()) + } + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setErrorMessage('') @@ -74,6 +94,7 @@ export function RecordRidePage() { const response = await recordRide(request) setSuccessMessage(`Ride recorded successfully (ID: ${response.rideId})`) + await loadQuickRideOptions() // Keep form values but clear after delay setTimeout(() => { @@ -102,6 +123,23 @@ export function RecordRidePage() { {successMessage &&
{successMessage}
} {errorMessage &&
{errorMessage}
} + {quickRideOptions.length > 0 && ( +
+

Quick Ride Options

+
+ {quickRideOptions.map((option, index) => ( + + ))} +
+
+ )} +
@@ -121,7 +159,16 @@ export function RecordRidePage() { type="number" step="0.01" value={miles} - onChange={(e) => setMiles(e.target.value)} + onChange={(e) => { + setMiles(e.target.value) + if (errorMessage.length > 0) { + setErrorMessage('') + } + }} + onInvalid={(e) => { + e.preventDefault() + setErrorMessage('Miles must be greater than 0') + }} required />
diff --git a/src/BikeTracking.Frontend/src/services/ridesService.test.ts b/src/BikeTracking.Frontend/src/services/ridesService.test.ts index 405bfb4..031b2b5 100644 --- a/src/BikeTracking.Frontend/src/services/ridesService.test.ts +++ b/src/BikeTracking.Frontend/src/services/ridesService.test.ts @@ -110,6 +110,29 @@ describe("ridesService", () => { expect(result.defaultMiles).toBe(10.5); }); + it("should return quick ride options from GET /api/rides/quick-options", async () => { + const response = { + options: [ + { + miles: 10.5, + rideMinutes: 45, + lastUsedAtLocal: "2026-03-30T07:30:00", + }, + ], + generatedAtUtc: new Date().toISOString(), + }; + fetchMock.mockResolvedValueOnce(jsonResponse(response, true)); + + const result = await ridesService.getQuickRideOptions(); + + expect(fetchMock).toHaveBeenCalledWith( + expect.stringContaining("/api/rides/quick-options"), + expect.objectContaining({ method: "GET" }), + ); + expect(result.options).toHaveLength(1); + expect(result.options[0].miles).toBe(10.5); + }); + it("should fetch ride history and return typed response", async () => { const response: ridesService.RideHistoryResponse = { summaries: { diff --git a/src/BikeTracking.Frontend/src/services/ridesService.ts b/src/BikeTracking.Frontend/src/services/ridesService.ts index 98c3693..e8e812c 100644 --- a/src/BikeTracking.Frontend/src/services/ridesService.ts +++ b/src/BikeTracking.Frontend/src/services/ridesService.ts @@ -20,6 +20,17 @@ export interface RideDefaultsResponse { defaultRideDateTimeLocal: string; } +export interface QuickRideOption { + miles: number; + rideMinutes: number; + lastUsedAtLocal: string; +} + +export interface QuickRideOptionsResponse { + options: QuickRideOption[]; + generatedAtUtc: string; +} + export interface EditRideRequest { rideDateTimeLocal: string; miles: number; @@ -183,6 +194,21 @@ export async function getRideDefaults(): Promise { return response.json(); } +export async function getQuickRideOptions(): Promise { + const response = await fetch(`${API_BASE_URL}/api/rides/quick-options`, { + method: "GET", + headers: getAuthHeaders(), + }); + + if (!response.ok) { + throw new Error( + await parseErrorMessage(response, "Failed to fetch quick ride options"), + ); + } + + return response.json(); +} + export async function editRide( rideId: number, request: EditRideRequest, diff --git a/src/BikeTracking.Frontend/tests/e2e/record-ride.spec.ts b/src/BikeTracking.Frontend/tests/e2e/record-ride.spec.ts index 860aab0..d0cf3d1 100644 --- a/src/BikeTracking.Frontend/tests/e2e/record-ride.spec.ts +++ b/src/BikeTracking.Frontend/tests/e2e/record-ride.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from "@playwright/test"; import { createAndLoginUser, uniqueUser } from "./support/auth-helpers"; -import { recordRide } from "./support/ride-helpers"; +import { recordRide, selectQuickRideOption } from "./support/ride-helpers"; const TEST_PIN = "87654321"; @@ -34,4 +34,27 @@ test.describe("004-record-ride e2e", () => { await expect(page.locator("#rideMinutes")).toHaveValue("35"); await expect(page.locator("#temperature")).toHaveValue("61"); }); + + test("allows editing quick-option copied values before save", async ({ + page, + }) => { + const userName = uniqueUser("e2e-quick-option-edit"); + await createAndLoginUser(page, userName, TEST_PIN); + + await recordRide(page, { + miles: "8.50", + rideMinutes: "30", + temperature: "60", + }); + + await page.goto("/rides/record"); + + await selectQuickRideOption(page, /8\.5 mi\s*-\s*30 min/i); + + await page.locator("#miles").fill("9.25"); + await page.locator("#rideMinutes").fill("33"); + + await page.getByRole("button", { name: "Record Ride" }).click(); + await expect(page.getByText(/ride recorded successfully/i)).toBeVisible(); + }); }); diff --git a/src/BikeTracking.Frontend/tests/e2e/support/ride-helpers.ts b/src/BikeTracking.Frontend/tests/e2e/support/ride-helpers.ts index 6568e18..783f71f 100644 --- a/src/BikeTracking.Frontend/tests/e2e/support/ride-helpers.ts +++ b/src/BikeTracking.Frontend/tests/e2e/support/ride-helpers.ts @@ -37,3 +37,10 @@ export async function recordRide( await page.getByRole("button", { name: "Record Ride" }).click(); await expect(page.getByText(/ride recorded successfully/i)).toBeVisible(); } + +export async function selectQuickRideOption( + page: Page, + labelPattern: RegExp, +): Promise { + await page.getByRole("button", { name: labelPattern }).click(); +} From 439302a917c6c7704086f3da0ad356aff74e17cf Mon Sep 17 00:00:00 2001 From: aligneddev Date: Mon, 30 Mar 2026 19:36:46 +0000 Subject: [PATCH 2/3] fix: restore api test host startup and quick-options compile --- .../Endpoints/Rides/DeleteRideEndpointTests.cs | 1 + .../Application/Rides/GetQuickRideOptionsService.cs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BikeTracking.Api.Tests/Endpoints/Rides/DeleteRideEndpointTests.cs b/src/BikeTracking.Api.Tests/Endpoints/Rides/DeleteRideEndpointTests.cs index 198c786..8a72260 100644 --- a/src/BikeTracking.Api.Tests/Endpoints/Rides/DeleteRideEndpointTests.cs +++ b/src/BikeTracking.Api.Tests/Endpoints/Rides/DeleteRideEndpointTests.cs @@ -118,6 +118,7 @@ public static async Task StartAsync() // Add Rides services builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/BikeTracking.Api/Application/Rides/GetQuickRideOptionsService.cs b/src/BikeTracking.Api/Application/Rides/GetQuickRideOptionsService.cs index 5ef83f2..4e4d2ae 100644 --- a/src/BikeTracking.Api/Application/Rides/GetQuickRideOptionsService.cs +++ b/src/BikeTracking.Api/Application/Rides/GetQuickRideOptionsService.cs @@ -34,7 +34,6 @@ public async Task ExecuteAsync( group.Max(ride => ride.RideDateTimeLocal) )) .OrderByDescending(option => option.LastUsedAtLocal) - .AsNoTracking() .Take(5) .ToList(); From aea65b3a5e2b9e170ecaca6a4a54e0c372ccaef6 Mon Sep 17 00:00:00 2001 From: aligneddev Date: Mon, 30 Mar 2026 19:41:09 +0000 Subject: [PATCH 3/3] 008 --- .../checklists/requirements.md | 0 .../contracts/quick-ride-options-api.yaml | 0 .../data-model.md | 2 +- .../plan.md | 6 +++--- .../quickstart.md | 2 +- .../research.md | 2 +- .../spec.md | 2 +- .../tasks.md | 8 ++++---- 8 files changed, 11 insertions(+), 11 deletions(-) rename specs/{001-quick-ride-entry => 008-quick-ride-entry}/checklists/requirements.md (100%) rename specs/{001-quick-ride-entry => 008-quick-ride-entry}/contracts/quick-ride-options-api.yaml (100%) rename specs/{001-quick-ride-entry => 008-quick-ride-entry}/data-model.md (99%) rename specs/{001-quick-ride-entry => 008-quick-ride-entry}/plan.md (96%) rename specs/{001-quick-ride-entry => 008-quick-ride-entry}/quickstart.md (99%) rename specs/{001-quick-ride-entry => 008-quick-ride-entry}/research.md (99%) rename specs/{001-quick-ride-entry => 008-quick-ride-entry}/spec.md (99%) rename specs/{001-quick-ride-entry => 008-quick-ride-entry}/tasks.md (97%) diff --git a/specs/001-quick-ride-entry/checklists/requirements.md b/specs/008-quick-ride-entry/checklists/requirements.md similarity index 100% rename from specs/001-quick-ride-entry/checklists/requirements.md rename to specs/008-quick-ride-entry/checklists/requirements.md diff --git a/specs/001-quick-ride-entry/contracts/quick-ride-options-api.yaml b/specs/008-quick-ride-entry/contracts/quick-ride-options-api.yaml similarity index 100% rename from specs/001-quick-ride-entry/contracts/quick-ride-options-api.yaml rename to specs/008-quick-ride-entry/contracts/quick-ride-options-api.yaml diff --git a/specs/001-quick-ride-entry/data-model.md b/specs/008-quick-ride-entry/data-model.md similarity index 99% rename from specs/001-quick-ride-entry/data-model.md rename to specs/008-quick-ride-entry/data-model.md index 26baba2..bd235e1 100644 --- a/specs/001-quick-ride-entry/data-model.md +++ b/specs/008-quick-ride-entry/data-model.md @@ -1,7 +1,7 @@ # Data Model: Quick Ride Entry from Past Rides **Feature**: Quick Ride Entry from Past Rides (001) -**Branch**: `001-quick-ride-entry` +**Branch**: `008-quick-ride-entry` **Date**: 2026-03-30 **Phase**: Phase 1 - Design & Contracts diff --git a/specs/001-quick-ride-entry/plan.md b/specs/008-quick-ride-entry/plan.md similarity index 96% rename from specs/001-quick-ride-entry/plan.md rename to specs/008-quick-ride-entry/plan.md index bcd4aad..edb489f 100644 --- a/specs/001-quick-ride-entry/plan.md +++ b/specs/008-quick-ride-entry/plan.md @@ -1,7 +1,7 @@ # Implementation Plan: Quick Ride Entry from Past Rides -**Branch**: `001-quick-ride-entry` | **Date**: 2026-03-30 | **Spec**: `/specs/001-quick-ride-entry/spec.md` -**Input**: Feature specification from `/specs/001-quick-ride-entry/spec.md` +**Branch**: `008-quick-ride-entry` | **Date**: 2026-03-30 | **Spec**: `/specs/008-quick-ride-entry/spec.md` +**Input**: Feature specification from `/specs/008-quick-ride-entry/spec.md` **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow. @@ -54,7 +54,7 @@ No constitutional violations identified. ### Documentation (this feature) ```text -specs/001-quick-ride-entry/ +specs/008-quick-ride-entry/ ├── plan.md ├── research.md ├── data-model.md diff --git a/specs/001-quick-ride-entry/quickstart.md b/specs/008-quick-ride-entry/quickstart.md similarity index 99% rename from specs/001-quick-ride-entry/quickstart.md rename to specs/008-quick-ride-entry/quickstart.md index 14b8301..8664fdc 100644 --- a/specs/001-quick-ride-entry/quickstart.md +++ b/specs/008-quick-ride-entry/quickstart.md @@ -1,7 +1,7 @@ # Quickstart: Quick Ride Entry from Past Rides **Feature**: Quick Ride Entry from Past Rides -**Branch**: `001-quick-ride-entry` +**Branch**: `008-quick-ride-entry` **Date**: 2026-03-30 ## Quick Reference diff --git a/specs/001-quick-ride-entry/research.md b/specs/008-quick-ride-entry/research.md similarity index 99% rename from specs/001-quick-ride-entry/research.md rename to specs/008-quick-ride-entry/research.md index 6f71b95..4896961 100644 --- a/specs/001-quick-ride-entry/research.md +++ b/specs/008-quick-ride-entry/research.md @@ -1,7 +1,7 @@ # Research: Quick Ride Entry from Past Rides **Feature**: Quick Ride Entry from Past Rides (001) -**Branch**: `001-quick-ride-entry` +**Branch**: `008-quick-ride-entry` **Date**: 2026-03-30 **Phase**: Phase 0 - Research & Decisions diff --git a/specs/001-quick-ride-entry/spec.md b/specs/008-quick-ride-entry/spec.md similarity index 99% rename from specs/001-quick-ride-entry/spec.md rename to specs/008-quick-ride-entry/spec.md index 5baee12..082ce4e 100644 --- a/specs/001-quick-ride-entry/spec.md +++ b/specs/008-quick-ride-entry/spec.md @@ -1,6 +1,6 @@ # Feature Specification: Quick Ride Entry from Past Rides -**Feature Branch**: `001-quick-ride-entry` +**Feature Branch**: `008-quick-ride-entry` **Created**: 2026-03-30 **Status**: Draft **Input**: User description: "Enable quick ride entry by allowing the user to pick from up to 5 distinct past rides. Copy in miles and duration when the user selects one. Many times my rides are the same most days." diff --git a/specs/001-quick-ride-entry/tasks.md b/specs/008-quick-ride-entry/tasks.md similarity index 97% rename from specs/001-quick-ride-entry/tasks.md rename to specs/008-quick-ride-entry/tasks.md index 156d9ff..d4216de 100644 --- a/specs/001-quick-ride-entry/tasks.md +++ b/specs/008-quick-ride-entry/tasks.md @@ -1,6 +1,6 @@ # Tasks: Quick Ride Entry from Past Rides -**Input**: Design documents from `/specs/001-quick-ride-entry/` +**Input**: Design documents from `/specs/008-quick-ride-entry/` **Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/quick-ride-options-api.yaml **Tests**: Tests are included because this feature plan requires a TDD-first workflow. @@ -10,7 +10,7 @@ **Purpose**: Align feature docs and local API playground artifacts before implementation. - [X] T001 Update API playground examples for quick options in src/BikeTracking.Api/BikeTracking.Api.http -- [X] T002 Sync quick options contract notes in specs/001-quick-ride-entry/quickstart.md +- [X] T002 Sync quick options contract notes in specs/008-quick-ride-entry/quickstart.md --- @@ -108,8 +108,8 @@ **Purpose**: Final consistency, docs, and full verification across all stories. - [X] T033 [P] Add quick options request helper coverage updates in src/BikeTracking.Frontend/tests/e2e/support/ride-helpers.ts -- [X] T034 [P] Update feature docs and acceptance notes in specs/001-quick-ride-entry/quickstart.md -- [X] T035 Run full verification matrix commands from specs/001-quick-ride-entry/quickstart.md and record results in specs/001-quick-ride-entry/tasks.md +- [X] T034 [P] Update feature docs and acceptance notes in specs/008-quick-ride-entry/quickstart.md +- [X] T035 Run full verification matrix commands from specs/008-quick-ride-entry/quickstart.md and record results in specs/008-quick-ride-entry/tasks.md ### Verification Results