import type { AccessCode, Device } from '@seamapi/types/connect' import classNames from 'classnames' import { DateTime } from 'luxon' import { useState } from 'react' import { useForm } from 'react-hook-form' import { getSystemTimeZone } from 'lib/dates.js' import { useGenerateAccessCodeCode } from 'lib/seam/access-codes/use-generate-access-code-code.js' import { AccessCodeFormDatePicker } from 'lib/ui/AccessCodeForm/AccessCodeFormDatePicker.js' import { AccessCodeFormTimes } from 'lib/ui/AccessCodeForm/AccessCodeFormTimes.js' import { AccessCodeFormTimeZonePicker } from 'lib/ui/AccessCodeForm/AccessCodeFormTimeZonePicker.js' import { Button } from 'lib/ui/Button.js' import { FormField } from 'lib/ui/FormField.js' import { InputLabel } from 'lib/ui/InputLabel.js' import { ContentHeader } from 'lib/ui/layout/ContentHeader.js' import { RadioField } from 'lib/ui/RadioField/RadioField.js' import { TextField } from 'lib/ui/TextField/TextField.js' import { useToggle } from 'lib/ui/use-toggle.js' export interface AccessCodeFormSubmitData { name: string code: string type: AccessCode['type'] device: Device startDate: string endDate: string timeZone: string } export interface ResponseErrors { unknown?: string | undefined code?: string | undefined } export interface AccessCodeFormProps { accessCode?: AccessCode device: Device isSubmitting: boolean onSubmit: (data: AccessCodeFormSubmitData) => void responseErrors: ResponseErrors | null onBack: (() => void) | undefined className: string | undefined } export function AccessCodeForm({ className, ...props }: AccessCodeFormProps): JSX.Element | null { return (
) } function Content({ onBack, accessCode, device, onSubmit, isSubmitting, responseErrors, }: Omit): JSX.Element { const [type, setType] = useState( accessCode?.type ?? 'ongoing' ) const [datePickerVisible, setDatePickerVisible] = useState(false) const [timeZone, setTimeZone] = useState(getSystemTimeZone()) const [startDate, setStartDate] = useState( getAccessCodeDate('starts_at', accessCode) ?? getNow(timeZone) ) const [endDate, setEndDate] = useState( getAccessCodeDate('ends_at', accessCode) ?? getOneDayFromNow(timeZone) ) const save = (data: { name: string; code: string }): void => { if (isSubmitting) { return } const start = startDate.toISO() if (start === null) { throw new Error(`Invalid start date: ${startDate.invalidReason}`) } const end = endDate.toISO() if (end === null) { throw new Error(`Invalid end date: ${endDate.invalidReason}`) } onSubmit({ name: data.name, code: data.code, type, device, startDate: start, endDate: end, timeZone, }) } const { register, handleSubmit, formState: { errors }, setValue, } = useForm({ defaultValues: { name: accessCode?.name ?? '', code: accessCode?.code ?? '', }, }) const [timeZonePickerVisible, toggleTimeZonePicker] = useToggle() const { isPending: isGeneratingCode, mutate: generateCode } = useGenerateAccessCodeCode() const submit = (): void => {} if (timeZonePickerVisible) { return ( { setTimeZone(timeZone) setStartDate(startDate.setZone(timeZone)) setEndDate(endDate.setZone(timeZone)) }} onClose={toggleTimeZonePicker} /> ) } if (datePickerVisible) { return ( { setDatePickerVisible(false) }} /> ) } const title = accessCode == null ? t.addNewAccessCode : t.editAccessCode const handleGenerateCode = (): void => { generateCode( { device_id: device.device_id, }, { onSuccess: (generatedCode) => { setValue('code', generatedCode) }, } ) } const hasCodeError = errors.code != null || responseErrors?.code != null const codeLengthRequirement = getCodeLengthRequirement(device) const codeLengthRequirementMessage = codeLengthRequirement != null ? t.codeLengthRequirement(codeLengthRequirement) : null const canSpecifyPinCode = device.properties.code_constraints?.every( ({ constraint_type: type }) => type !== 'cannot_specify_pin_code' ) ?? true const hasCodeInputs = accessCode?.is_offline_access_code !== true && canSpecifyPinCode return ( <>
{ void handleSubmit(save)(event) }} > {t.nameInputLabel} {hasCodeInputs && ( <> {t.codeInputLabel} validateCodeLength(device, value), }), }} />
{codeLengthRequirementMessage != null && (
  • {codeLengthRequirementMessage}
  • {t.codeNumbersOnlyRequirement}
)}
{t.timingInputLabel} <> {type === 'time_bound' && ( { setDatePickerVisible(true) }} /> )} )} {responseErrors?.unknown != null && (
{responseErrors?.unknown}
)}
) } const validateCodeLength = ( device: Device, value: string ): boolean | string => { if (device.properties.supported_code_lengths == null) { return true } if (device.properties.supported_code_lengths.includes(value.length)) { return true } return t.codeLengthError(device.properties.supported_code_lengths.join(', ')) } const getCodeLengthRequirement = (device: Device): string | null => { const codeLengths = device.properties.supported_code_lengths if (codeLengths == null) { return null } if (codeLengths.length === 1) { return `${codeLengths[0]}` } if (isSequential(codeLengths.join(''))) { return `${codeLengths[0]}-${codeLengths[codeLengths.length - 1]}` } return codeLengths.join(', ') } // 0 - 99 in a string // 0123456789101112...99 const sequentialNumbers = Array.from({ length: 100 }, (_, index) => index).join( '' ) const isSequential = (numbers: string): boolean => sequentialNumbers.includes(numbers) const getAccessCodeDate = ( key: 'starts_at' | 'ends_at', accessCode?: AccessCode ): DateTime | null => { if (accessCode == null) { return null } if (accessCode.type !== 'time_bound') { return null } const date = accessCode[key] if (date == null) { return null } return DateTime.fromISO(date) } const getNow = (timeZone: string): DateTime => DateTime.now().setZone(timeZone) const getOneDayFromNow = (timeZone: string): DateTime => DateTime.now().setZone(timeZone).plus({ days: 1 }) const t = { addNewAccessCode: 'Add new access code', editAccessCode: 'Edit access code', nameOverCharacterLimitError: '60 characters max', nameRequiredError: 'Name is required', nameInputLabel: 'Name the new code', codeGenerateButton: 'Generate code', codeInputLabel: 'Enter the code (PIN)', codeRequiredError: 'Code is required', codeLengthError: (lengths: string) => `Code length must be one of the following: ${lengths}`, codeLengthRequirement: (lengths: string) => `${lengths} digit code`, codeNumbersOnlyRequirement: 'Numbers only', cancel: 'Cancel', save: 'Save', timingInputLabel: 'Timing', typeOngoingLabel: 'Ongoing', typeTimeBoundLabel: 'Start/end times', }