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

Open
opened 2026-08-03 14:41:09 +00:00 by spikerj · 0 comments
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.
Sign in to join this conversation.