Design: trip planner — day-by-day itinerary with costs, materials, and calendar tie-in #906

Closed
opened 2026-08-03 14:41:09 +00:00 by spikerj · 1 comment
Owner

Problem

We can wishlist and approve destinations, and staff can set a flat per-country cost estimate, but there is no way to build the actual plan: a day-by-day breakdown of activities with time, location, materials/equipment, and per-activity cost. That breakdown is the most important input to understanding what a trip really costs a person/family and how the time is spent. It should also tie visually into the existing calendar.

Current state (verified 2026-08-03)

  • There is no Trip/Journey entity. Destinations are bare ISO strings ("HND", "US-LA") in parallel lists on TravelProfile (SpikerSoft.Data/Mongos/ProfileTravelSubDocuments.cs). No Country/Destination/Place entity.
  • Costs = UserProfile.TripCosts: List<TripCostEntry { Country, EstimatedCostCents, LineItems[{Label, AmountCents}], SetBy, SetAt }> — staff-only write (PUT /api/sponsor/profiles/{id}/trip-costs), flat free-text labels, no dates/days/activities. LocationFund, RewardEngine, and the sponsor cards all read TripCosts.
  • Calendar = generic FullCalendar personal/family calendar (Mongos/CalendarEvent.cs): RRULE recurrence, iCal import, parental access, GroupId, free-form ExtendedProps (BsonDocument), Location as plain string. Zero trip linkage today.
  • Nothing reusable named itinerary/activity: RedisOM/CalendarEvent.cs has a dead unreferenced List<TimeSpan> Itinerary (vestige — remove as part of this work); Activity/UserActivity is analytics telemetry (naming collision!); "Journey" is the lesson learning path.
  • No materials/equipment model exists anywhere in the codebase.

Design sketch

Data model — new top-level Mongo collection TripPlan (NOT embedded in UserProfile: unbounded growth, multi-member; also avoids the optional-owned-nav EF-Mongo landmine):

  • TripPlan { Id, OwnerProfileId, FamilyKey?, DestinationCode, Title, StartDate?, EndDate?, Status(draft/active/archived), Days: ItineraryDay[], audit }
  • ItineraryDay { Date? or DayNumber, Notes, Items: ItineraryItem[] }
  • ItineraryItem { Title, StartTime?, EndTime?, Location(string, matches CalendarEvent.Location), EquipmentNeeded: string[] (first materials model in the codebase), EstimatedCostCents, Notes }
  • Name it ItineraryItem/PlannedActivity — never Activity (collides with analytics UserActivity).

Cost roll-up — feed, don't replace. Plan totals (sum of item costs per destination) roll up INTO the existing TripCostEntry.LineItems shape rather than replacing TripCosts, since LocationFund/RewardEngine/sponsor cards read it. Staff PUT trip-costs remains; the planner becomes a second (user-side) source. Decide precedence/merge rules (e.g. planner-derived entries flagged with SetBy = "planner").

Calendar tie-in. Project plan items as CalendarEvents using the existing GroupId (one group per TripPlan) + ExtendedProps seam — no CalendarEvent schema change. Sync strategy (one-way projection with regenerate-on-save is simplest) to be decided.

Backend. SpikerSoft.Business/Domain/TripPlan/ (standard MediatR Commands/Queries/DTOs module) + SpikerSoft.Api/Domain/TripPlan/TripPlanController.cs + entity registered in SpikerDbContext. Owner + family-member authz (mirror calendar parental-access rules).

Frontend. libraries/features/trip-plan/ → @spikersoft/feature-trip-plan (+ libraries/domain/trip-plan/ state lib), lazy route in projects/spikersoft/src/routes.ts near calendar/sponsor; follow the feature-sponsor shape. Signal Forms + httpResource rules apply. Day editor UI: per-day item list with time range, location, equipment chips, cost; running per-day and per-trip totals.

Cleanup. Delete dead SpikerSoft.Data/RedisOM/CalendarEvent.cs to clear the Itinerary name collision.

Out of scope

In-kind sponsorship (accommodation/equipment donation) — explicitly dropped for now, Stripe-only remains.

## Problem We can wishlist and approve destinations, and staff can set a flat per-country cost estimate, but there is no way to build the *actual plan*: a day-by-day breakdown of activities with time, location, materials/equipment, and per-activity cost. That breakdown is the most important input to understanding what a trip really costs a person/family and how the time is spent. It should also tie visually into the existing calendar. ## Current state (verified 2026-08-03) - **There is no Trip/Journey entity.** Destinations are bare ISO strings (`"HND"`, `"US-LA"`) in parallel lists on `TravelProfile` (`SpikerSoft.Data/Mongos/ProfileTravelSubDocuments.cs`). No Country/Destination/Place entity. - Costs = `UserProfile.TripCosts: List<TripCostEntry { Country, EstimatedCostCents, LineItems[{Label, AmountCents}], SetBy, SetAt }>` — staff-only write (`PUT /api/sponsor/profiles/{id}/trip-costs`), flat free-text labels, no dates/days/activities. `LocationFund`, `RewardEngine`, and the sponsor cards all read TripCosts. - Calendar = generic FullCalendar personal/family calendar (`Mongos/CalendarEvent.cs`): RRULE recurrence, iCal import, parental access, `GroupId`, free-form `ExtendedProps` (BsonDocument), `Location` as plain string. Zero trip linkage today. - Nothing reusable named itinerary/activity: `RedisOM/CalendarEvent.cs` has a dead unreferenced `List<TimeSpan> Itinerary` (vestige — remove as part of this work); `Activity`/`UserActivity` is analytics telemetry (naming collision!); "Journey" is the lesson learning path. - No materials/equipment model exists anywhere in the codebase. ## Design sketch **Data model** — new top-level Mongo collection `TripPlan` (NOT embedded in UserProfile: unbounded growth, multi-member; also avoids the optional-owned-nav EF-Mongo landmine): - `TripPlan { Id, OwnerProfileId, FamilyKey?, DestinationCode, Title, StartDate?, EndDate?, Status(draft/active/archived), Days: ItineraryDay[], audit }` - `ItineraryDay { Date? or DayNumber, Notes, Items: ItineraryItem[] }` - `ItineraryItem { Title, StartTime?, EndTime?, Location(string, matches CalendarEvent.Location), EquipmentNeeded: string[] (first materials model in the codebase), EstimatedCostCents, Notes }` - Name it `ItineraryItem`/`PlannedActivity` — never `Activity` (collides with analytics `UserActivity`). **Cost roll-up — feed, don't replace.** Plan totals (sum of item costs per destination) roll up INTO the existing `TripCostEntry.LineItems` shape rather than replacing TripCosts, since LocationFund/RewardEngine/sponsor cards read it. Staff `PUT trip-costs` remains; the planner becomes a second (user-side) source. Decide precedence/merge rules (e.g. planner-derived entries flagged with `SetBy = "planner"`). **Calendar tie-in.** Project plan items as `CalendarEvent`s using the existing `GroupId` (one group per TripPlan) + `ExtendedProps` seam — no CalendarEvent schema change. Sync strategy (one-way projection with regenerate-on-save is simplest) to be decided. **Backend.** `SpikerSoft.Business/Domain/TripPlan/` (standard MediatR Commands/Queries/DTOs module) + `SpikerSoft.Api/Domain/TripPlan/TripPlanController.cs` + entity registered in `SpikerDbContext`. Owner + family-member authz (mirror calendar parental-access rules). **Frontend.** `libraries/features/trip-plan/` → `@spikersoft/feature-trip-plan` (+ `libraries/domain/trip-plan/` state lib), lazy route in `projects/spikersoft/src/routes.ts` near calendar/sponsor; follow the `feature-sponsor` shape. Signal Forms + httpResource rules apply. Day editor UI: per-day item list with time range, location, equipment chips, cost; running per-day and per-trip totals. **Cleanup.** Delete dead `SpikerSoft.Data/RedisOM/CalendarEvent.cs` to clear the Itinerary name collision. ## Out of scope In-kind sponsorship (accommodation/equipment donation) — explicitly dropped for now, Stripe-only remains.
Author
Owner

Migrated as part of the umbrella-tracker breakup.

This ticket needed changes in more than one repo, so it became one issue per repo:

Verified 2026-08-07 — the design's "Current state (verified 2026-08-03)" section still holds
exactly, nothing has been started.

  • Backend spikersoft-backend@98102023: git grep -l 'TripPlan|ItineraryDay|ItineraryItem' → nothing. Destinations are still bare
    ISO strings on ProfileTravelSubDocuments.cs; the dead SpikerSoft.Data/RedisOM/CalendarEvent.cs:15
    List<TimeSpan> Itinerary vestige is still present and unreferenced.
  • Angular spikersoft-angular@8e5a4048: no libraries/features/trip-plan/, no route, no domain lib; live bundle has no trip-plan.

Split because the model/API and the editor UI are separately mergeable units in different repos. The backend child
also carries the RedisOM/CalendarEvent.cs cleanup, since that file lives there.

Status: not started, both halves.

Closing here. Work now lives in the repos that hold the fixes, so fixes #N in a PR will auto-close them on merge. The umbrella tracker keeps cross-repo epics only.

— Opus 5 Agent

Migrated as part of the umbrella-tracker breakup. This ticket needed changes in more than one repo, so it became one issue per repo: - Backend: **spikerj/spikersoft-backend#573** - Angular: **spikerj/spikersoft-angular#685** Verified 2026-08-07 — the design's "Current state (verified 2026-08-03)" section still holds exactly, nothing has been started. - Backend `spikersoft-backend@98102023`: `git grep -l 'TripPlan|ItineraryDay|ItineraryItem'` → **nothing**. Destinations are still bare ISO strings on `ProfileTravelSubDocuments.cs`; the dead `SpikerSoft.Data/RedisOM/CalendarEvent.cs:15` `List<TimeSpan> Itinerary` vestige is still present and unreferenced. - Angular `spikersoft-angular@8e5a4048`: no `libraries/features/trip-plan/`, no route, no domain lib; live bundle has no `trip-plan`. Split because the model/API and the editor UI are separately mergeable units in different repos. The backend child also carries the `RedisOM/CalendarEvent.cs` cleanup, since that file lives there. Status: not started, both halves. Closing here. Work now lives in the repos that hold the fixes, so `fixes #N` in a PR will auto-close them on merge. The umbrella tracker keeps cross-repo epics only. — Opus 5 Agent
Sign in to join this conversation.