import type { AccessCode } from '@seamapi/types/connect' import classNames from 'classnames' import { DateTime } from 'luxon' import { useCallback, useEffect, useState } from 'react' import { CopyIcon } from 'lib/icons/Copy.js' import { useAccessCode } from 'lib/seam/access-codes/use-access-code.js' import { useDeleteAccessCode } from 'lib/seam/access-codes/use-delete-access-code.js' import { AccessCodeDevice } from 'lib/seam/components/AccessCodeDetails/AccessCodeDevice.js' import { type CommonProps, withRequiredCommonProps, } from 'lib/seam/components/common-props.js' import { NestedDeviceDetails } from 'lib/seam/components/DeviceDetails/DeviceDetails.js' import { NestedEditAccessCodeForm } from 'lib/seam/components/EditAccessCodeForm/EditAccessCodeForm.js' import { useDevice } from 'lib/seam/devices/use-device.js' import { accessCodeErrorFilter, accessCodeWarningFilter, } from 'lib/seam/filters.js' import { useComponentTelemetry } from 'lib/telemetry/index.js' import { Alerts } from 'lib/ui/Alert/Alerts.js' import { Button } from 'lib/ui/Button.js' import { copyToClipboard } from 'lib/ui/clipboard.js' import { IconButton } from 'lib/ui/IconButton.js' import { ContentHeader } from 'lib/ui/layout/ContentHeader.js' import { Snackbar } from 'lib/ui/Snackbar/Snackbar.js' import { useIsDateInPast } from 'lib/ui/use-is-date-in-past.js' export interface AccessCodeDetailsProps extends CommonProps { accessCodeId: string onEdit?: () => void preventDefaultOnEdit?: boolean onDelete?: () => void preventDefaultOnDelete?: boolean } export const NestedAccessCodeDetails = withRequiredCommonProps(AccessCodeDetails) export function AccessCodeDetails({ accessCodeId, onEdit, preventDefaultOnEdit = false, onDelete, preventDefaultOnDelete = false, errorFilter = () => true, warningFilter = () => true, disableCreateAccessCode = false, disableEditAccessCode = false, disableLockUnlock = false, disableDeleteAccessCode = false, disableResourceIds = false, disableConnectedAccountInformation = false, onBack, className, }: AccessCodeDetailsProps): JSX.Element | null { useComponentTelemetry('AccessCodeDetails') const { accessCode } = useAccessCode({ access_code_id: accessCodeId }) const [selectedDeviceId, selectDevice] = useState(null) const { mutate: deleteCode, isPending: isDeleting } = useDeleteAccessCode() const [editFormOpen, setEditFormOpen] = useState(false) const [accessCodeResult, setAccessCodeResult] = useState< 'updated' | 'deleted' | null >(null) const [snackbarMessage, setSnackbarMessage] = useState('') // Circumvent Snackbar bug that causes it to switch to default message // while the dismiss animation is playing useEffect(() => { if (accessCodeResult !== null) { setSnackbarMessage(accessCodeResultToMessage(accessCodeResult)) } }, [accessCodeResult]) const handleEdit = useCallback((): void => { onEdit?.() if (preventDefaultOnEdit) return setEditFormOpen(true) }, [onEdit, preventDefaultOnEdit, setEditFormOpen]) const handleDelete = useCallback((): void => { onDelete?.() if (preventDefaultOnDelete) return if (accessCode == null) return deleteCode( { access_code_id: accessCode.access_code_id }, { onSuccess: () => { setAccessCodeResult('deleted') }, } ) }, [accessCode, deleteCode, onDelete, preventDefaultOnDelete]) const { device } = useDevice({ device_id: accessCode?.device_id }) const canSpecifyPinCode = device?.properties.code_constraints?.every( ({ constraint_type: type }) => type !== 'cannot_specify_pin_code' ) ?? true if (accessCode == null) { return null } const name = accessCode.name ?? t.fallbackName const isAccessCodeBeingRemoved = accessCode.status === 'removing' if (editFormOpen) { return ( { setEditFormOpen(false) }} onSuccess={() => { setAccessCodeResult('updated') setEditFormOpen(false) }} className={className} /> ) } if (selectedDeviceId != null) { return ( { selectDevice(null) }} className={className} /> ) } const alerts = [ ...accessCode.errors .filter(accessCodeErrorFilter) .filter(errorFilter) .map((error) => ({ variant: 'error' as const, message: error.message, })), ...accessCode.warnings .filter(accessCodeWarningFilter) .filter(warningFilter) .map((warning) => ({ variant: 'warning' as const, message: warning.message, })), ...(isAccessCodeBeingRemoved ? [ { variant: 'warning' as const, message: t.warningRemoving, }, ] : []), ] return ( <> { setAccessCodeResult(null) }} />
0 && 'seam-top-has-alerts' )} > {canSpecifyPinCode && ( <> {t.accessCode}
{name}
{accessCode.code} { void copyToClipboard(accessCode.code ?? '') }} >
)}
{canSpecifyPinCode && (!disableEditAccessCode || !disableDeleteAccessCode) && (
{!disableEditAccessCode && !accessCode.is_offline_access_code && ( )} {!disableDeleteAccessCode && !accessCode.is_offline_access_code && ( )}
)}
{!disableResourceIds && (
{t.id}:
{accessCode.access_code_id} { void copyToClipboard(accessCode.access_code_id) }} >
)}
{t.created}:
{formatDate(accessCode.created_at)}
{t.timing}:
) } function ScheduleInfo({ accessCode }: { accessCode: AccessCode }): JSX.Element { if (accessCode.type === 'ongoing') { return {t.ongoing} } return (
{accessCode.starts_at != null && (
{t.start}
{formatDate(accessCode.starts_at)}
{formatTime(accessCode.starts_at)}
)} {accessCode.ends_at != null && (
{t.end}
{formatDate(accessCode.ends_at)}
{formatTime(accessCode.ends_at)}
)}
) } function Duration(props: { accessCode: AccessCode }): JSX.Element | null { const { accessCode } = props const hasStarted = useIsDateInPast('starts_at' in accessCode ? accessCode?.starts_at : null) ?? false if (accessCode.type === 'ongoing') { return ( Active (ongoing) ) } if (hasStarted && accessCode.ends_at != null) { return ( {t.active} {t.until}{' '} {formatDurationDate(accessCode.ends_at)} {t.at}{' '} {formatTime(accessCode.ends_at)} ) } if (accessCode.starts_at != null) { return ( {t.starts} {formatDurationDate(accessCode.starts_at)} {t.as}{' '} {formatTime(accessCode.starts_at)} ) } return null } const formatDurationDate = (date: string): string => DateTime.fromISO(date).toLocaleString({ month: 'short', day: 'numeric', }) const formatTime = (date: string): string => DateTime.fromISO(date).toLocaleString({ hour: 'numeric', minute: '2-digit', }) const formatDate = (date: string): string => DateTime.fromISO(date).toLocaleString({ weekday: 'short', month: 'long', day: 'numeric', year: 'numeric', }) const accessCodeResultToMessage = (result: 'updated' | 'deleted'): string => { if (result === 'deleted') return t.accessCodeDeleted return t.accessCodeUpdated } const t = { accessCode: 'Access code', fallbackName: 'Code', id: 'ID', created: 'Created', timing: 'Timing', ongoing: 'Ongoing', start: 'Start', end: 'End', starts: 'Starts', active: 'Active', until: 'until', as: 'as', at: 'at', editCode: 'Edit code', deleteCode: 'Delete code', warningRemoving: 'This access code is currently being removed.', accessCodeUpdated: 'Access code updated', accessCodeDeleted: 'Access code is being removed', }