feat(marina): end-reservation UI + global list, yacht tabs, dashboard distinct count

- End-reservation: API handler existed but had no UI surface. Adds an
  "End reservation" button + date dialog on the reservation detail page,
  visible only when status is `active`.
- New port-scoped `GET /api/v1/berth-reservations` list endpoint and
  `[portSlug]/berth-reservations` page so users can see all reservations
  across all berths from one place (was 404).
- Berths "Edit" menu pushed `/berths/{id}?edit=true` but the detail page
  never read the param — it now auto-opens the edit sheet on mount and
  strips `edit` from the URL.
- Reservation detail no longer shows raw 8-char UUIDs for Berth / Yacht
  / Client; reuses the lazy-fetching link components from the list view.
- Yacht "Interests" and "Reservations" tabs replaced their "Coming soon"
  stubs with real lists fetched from the existing service routes.
- Dashboard "Pipeline Value" KPI used `select(berthId, price)` and
  summed per active interest, so a berth with three open interests was
  counted three times. Switched to `selectDistinct(berthId, price)`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-02 23:01:15 +02:00
parent e3e0e69c04
commit a391934b73
8 changed files with 301 additions and 44 deletions

View File

@@ -27,9 +27,11 @@ export async function getKpis(portId: string) {
.from(interests)
.where(and(eq(interests.portId, portId), isNull(interests.archivedAt), isActiveInterest));
// Pipeline value: SUM berths.price via JOIN from non-archived interests with berthId
// Pipeline value: SUM each berth's price ONCE regardless of how many active
// interests reference it. A berth with multiple interests would otherwise be
// counted multiple times, inflating the total.
const pipelineRows = await db
.select({ price: berths.price })
.selectDistinct({ berthId: interests.berthId, price: berths.price })
.from(interests)
.innerJoin(berths, eq(interests.berthId, berths.id))
.where(