Ctrl K
ComunidadGuías…

src/lib/trip-parts.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// A trip is a sequence of parts, and the two things every surface wants from
// that sequence are the span it covers and the places it went. Both are derived
// here rather than at each call site, because a list cell, a page subtitle and a
// Home card disagreeing about where a trip was is invisible until someone
// holds two of them side by side.

import type { Location } from "@/lib/api/location";
import type { TripPart } from "@/lib/api/trips";
import { formatTripDateRange } from "@/lib/date-time";

export interface TripSpan {
  start?: string;
  end?: string;
}

/**
 * A trip's span: the earliest `start_date` and the latest `end_date` across its
 * parts.
 *
 * Each end is taken independently, matching the API's own derivation. A trip
 * whose only dated part carries an end and no start has an end and no start -
 * what the data says, rather than an invented range. A trip with no parts, or
 * none carrying dates, has neither, which is a state the app has never had
 * before: every caller has to answer for it rather than format an absence.
 *
 * Dates are bare `YYYY-MM-DD`, so they compare as strings - no `Date` is
 * constructed here, which is the same reason `formatDateOnly` splits them
 * (DECISIONS.md, "Bare `YYYY-MM-DD` dates must not go through
 * `new Date(dateString)`").
 */
export function tripSpan(parts?: TripPart[] | null): TripSpan {
  let start: string | undefined;
  let end: string | undefined;
  for (const part of parts ?? []) {
    if (part.start_date && (start === undefined || part.start_date < start)) {
      start = part.start_date;
    }
    if (part.end_date && (end === undefined || part.end_date > end)) {
      end = part.end_date;
    }
  }
  return { start, end };
}

/**
 * A trip's span, formatted the way every surface shows it, or `undefined` when
 * its parts carry no dates at all.
 *
 * `formatTripDateRange` takes two dates and is shared with the courses pages,
 * so the reduction happens here and its signature is left alone. `undefined`
 * rather than "" for the same reason it gives: a table wants "-" where a
 * subtitle wants to disappear.
 */
export function formatTripSpan(parts?: TripPart[] | null): string | undefined {
  const { start, end } = tripSpan(parts);
  return formatTripDateRange(start, end);
}

/**
 * The places a trip went, in the diver's order, skipping the parts that have
 * none.
 *
 * What the label helpers and the maps take: both were written against a flat
 * list of locations and still are, because a dive site's page and a dive's
 * sidebar hand them theirs (`formatTripLocationNames`,
 * `components/map/map-backdrop.tsx`). A placeless part contributes nothing
 * here rather than an unnamed gap.
 */
export function tripPartLocations(parts?: TripPart[] | null): Location[] {
  return (parts ?? [])
    .map((part) => part.location)
    .filter((location): location is Location => !!location);
}

/**
 * One part's dates as the trip page shows them, or `undefined` when it has
 * none. A part with one date reads "From" or "Until" it, rather than as a
 * single day: a part with only a start runs on from it, and the trip page
 * groups dives that way (`tripPartForDay`).
 */
export function formatTripPartDates(part: TripPart): string | undefined {
  const start = part.start_date ?? undefined;
  const end = part.end_date ?? undefined;
  if (start && !end) return `From ${formatTripDateRange(start)}`;
  if (end && !start) return `Until ${formatTripDateRange(undefined, end)}`;
  return formatTripDateRange(start, end);
}