Ctrl K
ComunidadGuías…

src/components/checkin/checkin-page-content.tsx

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

Archivo original ↗Preguntar a KiBot
"use client";

import { useCallback, useEffect, useState } from "react";

import { useCheckinDetails } from "@/contexts/CheckinDetailsContext";
import { useAuthGuard } from "@/hooks/useAuthGuard";
import { useCheckinLink } from "@/hooks/useCheckinLink";
import { useUnits } from "@/hooks/useUnits";
import { useSavedElsewhere } from "@/hooks/useSavedElsewhere";
import { CheckInPageFrame } from "@/components/checkin/checkin-page-frame";
import {
  fetchAllCertifications,
  type Certification,
} from "@/lib/api/certifications";
import { diveStatsAPI, type UserDiveStats } from "@/lib/api/dive-stats";
import { divesAPI } from "@/lib/api/dives";
import { fetchAllContacts, type Contact } from "@/lib/api/contacts";
import { fetchAllPeople, type Person } from "@/lib/api/people";
import { ownCheckInDiver } from "@/lib/checkin";
import { PageSpinner } from "@/components/ui/page-spinner";

// The summary a diver hands to a dive shop. `CheckInPageFrame` draws it; this reads
// what the page does not already hold. The name and portrait arrive with the
// signed-in user, so they are on screen at the click; the check-in details come from
// the shared copy, which on a full load of this page is still in flight, and their
// sections hold their shape until it lands.
export function CheckInPageContent() {
  const { user, isAuthenticated, isLoading } = useAuthGuard();
  const checkin = useCheckinDetails();
  const units = useUnits();
  const [certifications, setCertifications] = useState<Certification[]>([]);
  const [contacts, setContacts] = useState<Contact[]>([]);
  const [people, setPeople] = useState<Person[]>([]);
  const [stats, setStats] = useState<UserDiveStats | null>(null);
  const [lastDiveAt, setLastDiveAt] = useState<string | null>(null);
  const [isSummaryLoading, setIsSummaryLoading] = useState(true);
  const [loadFailed, setLoadFailed] = useState(false);
  const [figuresFailed, setFiguresFailed] = useState(false);
  const [attempt, setAttempt] = useState(0);

  // Keyed on the uuid rather than on `user`: the auth context replaces that object
  // whenever anything on the account is saved, and a re-fetch of the whole summary
  // on each of those would be a round of requests for values none of them changed.
  //
  // A plain effect, not `useEffectOnChange`: this one starts requests and abandons
  // them in its cleanup, so guarding on deps would cancel on hide and skip on show
  // and the page would never load. Re-reading when a hidden route comes back is what
  // the data hooks do deliberately - a dive logged elsewhere has already made these
  // three figures stale.
  const userUuid = user?.uuid;
  const sharing = useCheckinLink(!!userUuid);
  useEffect(() => {
    if (!userUuid) return;

    const controller = new AbortController();

    // Settled rather than `all`: they answer different questions, and one of them
    // failing must not take the ones that arrived off the page with it. The c-cards
    // are the half a desk actually reads, and they do not depend on the dive count.
    const load = async () => {
      const [cards, diveStats, recent, contactList, peopleList] =
        await Promise.allSettled([
          // Every page of them: a diver holds a handful of cards, and a summary that
          // stopped at ten would leave one off the page at the desk.
          fetchAllCertifications(controller.signal),
          diveStatsAPI.getDiveStats(),
          // The last dive is read off the list rather than stored: `GET /dives` is
          // sorted by start time descending, so the first row of the first page is
          // it. Same read the new-dive form makes to carry a dive forward.
          divesAPI.getDives(1, 1),
          // The dive centers and the instructors the cards name, which a card
          // holds only by uuid. Each whole list in one read rather than one per
          // card, and alongside the rest rather than after the cards: a diver
          // keeps tens.
          fetchAllContacts(controller.signal),
          fetchAllPeople(controller.signal),
        ]);
      if (controller.signal.aborted) return;

      if (cards.status === "fulfilled") setCertifications(cards.value);
      if (contactList.status === "fulfilled") setContacts(contactList.value);
      if (peopleList.status === "fulfilled") setPeople(peopleList.value);
      if (diveStats.status === "fulfilled") setStats(diveStats.value);
      if (recent.status === "fulfilled") {
        setLastDiveAt(recent.value.data[0]?.start_time ?? null);
      }

      // Said out loud rather than swallowed the way Home's supplementary
      // cards swallow theirs: this page is handed to somebody else, and a summary
      // quietly missing its certifications is worse than one that says so. What
      // did arrive stays on screen regardless.
      const failed = [cards, diveStats, recent, contactList, peopleList].filter(
        (result) => result.status === "rejected",
      );
      for (const result of failed) {
        console.error("Failed to load part of the check-in summary:", result);
      }
      setLoadFailed(failed.length > 0);
      setFiguresFailed(
        diveStats.status === "rejected" || recent.status === "rejected",
      );
      setIsSummaryLoading(false);
    };

    load();
    return () => controller.abort();
  }, [userUuid, attempt]);

  // After a card is edited from the summary itself. Only the lists are re-read -
  // the cards, and the contacts and people, since the edit may have made one to
  // name - and the
  // stats and the last dive cannot have moved. The loading flag is left alone,
  // so the cards already on screen stay put rather than flashing back to skeletons.
  // A re-read rather than patching the saved card in place: the order is
  // `GET /certifications`' (`certified_on` descending, nulls last), and an edited
  // `certified_on` has to land where that puts it.
  const refreshCertifications = useCallback(async () => {
    try {
      const [cards, contactList, peopleList] = await Promise.all([
        fetchAllCertifications(),
        fetchAllContacts(),
        fetchAllPeople(),
      ]);
      setCertifications(cards);
      setContacts(contactList);
      setPeople(peopleList);
    } catch (error) {
      console.error("Failed to re-read the certifications:", error);
      setLoadFailed(true);
    }
  }, []);

  // The header's bell edits certifications too, from over this page.
  useSavedElsewhere("certification", () => void refreshCertifications());

  if (isLoading) {
    return <PageSpinner variant="inset" />;
  }

  if (!isAuthenticated || !user) {
    return null; // Will redirect to signin
  }

  const contactNames = namesByCertification(
    certifications,
    contacts,
    (certification) => certification.contact_uuid,
  );
  const instructorNames = namesByCertification(
    certifications,
    people,
    (certification) => certification.instructor_uuid,
  );

  return (
    <CheckInPageFrame
      diver={ownCheckInDiver(user, checkin.details)}
      details={
        checkin.details ? undefined : checkin.loadFailed ? "failed" : "loading"
      }
      units={units}
      certifications={certifications}
      contactNames={contactNames}
      instructorNames={instructorNames}
      stats={stats}
      lastDiveAt={lastDiveAt}
      isLoading={isSummaryLoading}
      loadFailed={loadFailed || checkin.loadFailed}
      figuresFailed={figuresFailed}
      onCertificationsChanged={refreshCertifications}
      sharing={sharing}
      onRetry={() => {
        if (checkin.loadFailed) checkin.reload();
        setIsSummaryLoading(true);
        setLoadFailed(false);
        setFiguresFailed(false);
        setAttempt((n) => n + 1);
      }}
    />
  );
}

// The name of the record each card names - its dive center, its instructor - by
// the card's own uuid, the shape the frame takes. A card naming one the list no
// longer holds simply has no row.
function namesByCertification(
  certifications: Certification[],
  records: { uuid: string; name: string }[],
  uuidOf: (certification: Certification) => string | null | undefined,
): Record<string, string> {
  const names = new Map(records.map((record) => [record.uuid, record.name]));
  const byCard: Record<string, string> = {};
  for (const certification of certifications) {
    const uuid = uuidOf(certification);
    const name = uuid ? names.get(uuid) : undefined;
    if (name) byCard[certification.uuid] = name;
  }
  return byCard;
}