Ctrl K
ComunidadGuías…

src/components/courses/training-fields.tsx

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

Archivo original ↗Preguntar a KiBot
"use client";

import type { ReactNode } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import {
  CERTIFICATION_AGENCIES,
  certificationAgencyLabel,
} from "@/lib/api/certifications";
import type { ContactRole } from "@/lib/api/contacts";
import {
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { NativeSelect } from "@/components/ui/native-select";
import { Input } from "@/components/ui/input";
import { DatePicker, rangeEndAfter } from "@/components/ui/date-picker";
import { ContactCombobox } from "@/components/contacts/contact-combobox";
import { PersonCombobox } from "@/components/people/person-combobox";

// What a contact created from a course or a card starts as: whoever ran the
// training.
const SCHOOL: readonly ContactRole[] = ["school"];

interface TrainingDateField {
  name: string;
  label: string;
}

interface TrainingFieldsProps {
  // A card always names its agency; a course run by a private instructor need
  // not, so its form offers "No agency" and stores `null`.
  agencyRequired: boolean;
  // The field sharing the agency's row.
  besideAgency: ReactNode;
  // Start, then end: the pickers treat the pair as a range.
  dates: readonly [TrainingDateField, TrainingDateField];
  // The date the pickers rank by last use at or before.
  until?: string;
  // Replaces the instructor field's own `onChange`, for a form that does more
  // when the instructor changes.
  onInstructorChange?: (uuid: string | null) => void;
}

// The block a course and a certification share: who ran the training, when, and
// who taught it. Both forms hold these under the same names - `agency`,
// `agency_other`, `contact_uuid`, `instructor_uuid`, `instructor_number` - and
// name their own pair of dates.
export function TrainingFields({
  agencyRequired,
  besideAgency,
  dates,
  until,
  onInstructorChange,
}: TrainingFieldsProps) {
  const { control, setValue } = useFormContext();
  // `agency_other` is only shown - and only accepted by the API - for "other".
  const agency = useWatch({ control, name: "agency" });
  const [startField, endField] = dates;
  const [start = "", end = ""] = useWatch({
    control,
    name: [startField.name, endField.name],
  }) as [string | undefined, string | undefined];

  const onStartChange = (value: string) => {
    const kept = rangeEndAfter(value, end);
    if (kept !== end) {
      setValue(endField.name, kept, {
        shouldDirty: true,
        shouldValidate: true,
      });
    }
  };

  return (
    <>
      <div className="grid gap-4 sm:grid-cols-2">
        <FormField
          control={control}
          name="agency"
          render={({ field }) => (
            <FormItem>
              <FormLabel>{agencyRequired ? "Agency *" : "Agency"}</FormLabel>
              <FormControl
                render={
                  <NativeSelect
                    {...field}
                    value={field.value ?? ""}
                    onChange={(e) => field.onChange(e.target.value || null)}
                  />
                }
              >
                {/* First, because it is what a course opens on, and the
                      state has to be pickable again after an agency has been
                      chosen. */}
                {!agencyRequired && <option value="">No agency</option>}
                {CERTIFICATION_AGENCIES.map((value) => (
                  <option key={value} value={value}>
                    {certificationAgencyLabel(value)}
                  </option>
                ))}
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        {besideAgency}
      </div>

      {/* Under the row rather than inside it: it belongs to the agency box above
          and appears only for one of its values, so a third box arriving in the
          middle of a filled row is a worse surprise than a fourth line. */}
      {agency === "other" && (
        <FormField
          control={control}
          name="agency_other"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Agency name *</FormLabel>
              <FormControl
                render={
                  <Input
                    placeholder="e.g. SwedTech Diving"
                    {...field}
                    value={field.value ?? ""}
                  />
                }
              />
              <FormMessage />
            </FormItem>
          )}
        />
      )}

      {/* Neither date is required: a course that is only booked has none yet,
          and most recreational cards never expire. */}
      <div className="grid gap-4 sm:grid-cols-2">
        {dates.map(({ name, label }, position) => (
          <FormField
            key={name}
            control={control}
            name={name}
            render={({ field }) => (
              <FormItem>
                <FormLabel>{label}</FormLabel>
                <FormControl
                  render={
                    <DatePicker
                      value={field.value ?? ""}
                      onChange={(value) => {
                        field.onChange(value);
                        if (position === 0) onStartChange(value);
                      }}
                      rangeEnd={position === 0 ? end : undefined}
                      rangeStart={position === 1 ? start : undefined}
                    />
                  }
                />
                <FormMessage />
              </FormItem>
            )}
          />
        ))}
      </div>

      <FormField
        control={control}
        name="contact_uuid"
        render={({ field }) => (
          <FormItem>
            <FormLabel>Dive center</FormLabel>
            {/* Its dialog opens over this one, and over the certification
                dialog too when a course is being added from there. */}
            <FormControl
              render={
                <ContactCombobox
                  value={field.value}
                  onChange={field.onChange}
                  until={until}
                  initialRoles={SCHOOL}
                  placeholder="Select a dive center..."
                  addNewLabel="Add dive center..."
                />
              }
            />
            <FormMessage />
          </FormItem>
        )}
      />

      <div className="grid gap-4 sm:grid-cols-2">
        <FormField
          control={control}
          name="instructor_uuid"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Instructor</FormLabel>
              {/* A person, picked or typed: a name nobody has yet becomes one
                  on Enter, so naming a new instructor is still one step. */}
              <FormControl
                render={
                  <PersonCombobox
                    value={field.value}
                    onChange={onInstructorChange ?? field.onChange}
                    until={until}
                    placeholder="Select an instructor..."
                    addNewLabel="Add instructor..."
                  />
                }
              />
              <FormMessage />
            </FormItem>
          )}
        />

        <FormField
          control={control}
          name="instructor_number"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Instructor number</FormLabel>
              <FormControl
                render={<Input {...field} value={field.value ?? ""} />}
              />
              <FormMessage />
            </FormItem>
          )}
        />
      </div>
    </>
  );
}