UNPKG

react-appointment-scheduler

Version:

A production-ready React scheduler component for appointment management with day/week views, drag-and-drop, and TypeScript support

913 lines (828 loc) 31.1 kB
import { DragEndEvent } from '@dnd-kit/core'; import { DragStartEvent } from '@dnd-kit/core'; import { JSX } from 'react/jsx-runtime'; import { Modifier } from '@dnd-kit/core'; import { NamedExoticComponent } from 'react'; /** * Adds minutes to a date and returns a new Date */ export declare function addMinutes(date: Date, minutes: number): Date; /** * Represents a single appointment in the scheduler */ export declare interface Appointment { /** Unique identifier for the appointment */ id: string; /** Name of the client */ client: Client; /** Optional list of jobs (multi-service appointment support) */ jobs?: Job[]; /** Type of lash service being performed */ serviceType: ServiceType; /** Lash artist: string id or object { id, name } (objects supported when data comes from APIs) */ artist?: Artist; /** Start time of the appointment */ startTime: Date; /** Duration of the appointment in minutes */ duration: number; /** Optional notes about the appointment */ notes?: string; /** Appointment status */ status: AppointmentStatus; /** Optional phone number for client contact */ phone?: string; /** Email address for client contact */ email: string; } /** * Memoized AppointmentBlock for performance */ export declare const AppointmentBlock: NamedExoticComponent<AppointmentBlockProps>; /** * AppointmentBlock Component * * Renders a single appointment as a positioned block within the time grid. * * Features: * - Positioned absolutely based on start time and duration * - Handles overlapping appointments by adjusting width and horizontal position * - Color-coded by service type * - Draggable for rescheduling (uses @dnd-kit) * - Displays client name, service type, and time range */ declare interface AppointmentBlockProps { /** Layout information including position and overlap data */ layout: AppointmentLayout; /** Callback when the appointment is clicked */ onClick?: (appointment: Appointment) => void; /** Whether this appointment is currently selected */ isSelected?: boolean; /** Whether this appointment is being dragged */ isDragging?: boolean; } /** * Layout information for rendering an appointment block */ export declare interface AppointmentLayout { /** The appointment data */ appointment: Appointment; /** Lane index for overlapping appointments (0-based) */ lane: number; /** Total number of lanes in this overlap group */ totalLanes: number; /** Calculated top position in pixels */ top: number; /** Calculated height in pixels */ height: number; /** Resolved color for this block (from technician or default) */ color?: string; } /** Status options for appointments */ export declare type AppointmentStatus = 'pending' | 'confirmed' | 'cancelled' | 'completed'; /** Artist can be a string id or an object with id and optional name (e.g. from APIs) */ declare type Artist = string | { id: string; name?: string; }; /** * Calculates layout information for all appointments * Handles overlap detection and assigns lanes for proper positioning * * @param appointments - Array of appointments to layout * @param gridStartHour - The hour the grid starts at (for calculating top position) * @returns Array of AppointmentLayout objects with position data */ export declare function calculateAppointmentLayouts(appointments: Appointment[], gridStartHour: number): AppointmentLayout[]; /** * Calculates total grid height in pixels */ export declare function calculateGridHeight(startHour: number, endHour: number, slotHeight: number): number; /** * Calculates the height in pixels for an appointment based on its duration * * @param durationMinutes - Duration in minutes * @returns Height in pixels */ export declare function calculateHeight(durationMinutes: number): number; /** * Calculates the top position in pixels for an appointment * based on its start time relative to the grid's start hour * * @param startTime - The appointment's start time * @param gridStartHour - The hour the grid starts at * @returns Top position in pixels */ export declare function calculateTopPosition(startTime: Date, gridStartHour: number): number; /** * Calculates the number of time slots for a given hour range * Each hour has 2 slots (30-minute intervals) */ export declare function calculateTotalSlots(startHour: number, endHour: number): number; declare type Client = { name: string; path: string; }; export declare const CreateAppointmentModal: NamedExoticComponent<CreateAppointmentModalProps>; declare interface CreateAppointmentModalProps { /** Whether the modal is open */ isOpen: boolean; /** Callback to close the modal */ onClose: () => void; /** Callback when appointment is created */ onCreate: (appointment: NewAppointmentData) => void; /** Pre-selected start time (from slot click) */ initialStartTime?: Date | null; /** Pre-selected end time (from slot click) */ initialEndTime?: Date | null; /** Pre-selected technician ID (from slot click in day view) */ initialTechnicianId?: string | null; /** List of available technicians with id and name */ technicians?: Technician[]; /** List of available services with id, name, and category */ services: Service[]; /** * Map of technician IDs to service IDs they can perform. * When provided, technician dropdown in the job builder will be * filtered based on the selected service. */ technicianServices?: TechnicianServices; } /** * Creates a Date object from a date and separate hour/minute values */ export declare function createDateTime(date: Date, hour: number, minute: number): Date; /** * DatePickerModal Component * * A modal dialog for selecting dates. * - In 'single' mode: select a single date * - In 'range' mode: select a date range (max 7 days by default) */ export declare function DatePickerModal({ isOpen, onClose, mode, initialDate, initialEndDate, maxRangeDays, onSelectDate, onSelectRange, }: DatePickerModalProps): JSX.Element | null; declare interface DatePickerModalProps { isOpen: boolean; onClose: () => void; mode: 'single' | 'range'; initialDate?: Date; initialEndDate?: Date; maxRangeDays?: number; onSelectDate?: (date: Date) => void; onSelectRange?: (startDate: Date, endDate: Date) => void; } /** * Open/close hours for a single day of the week. * Used when business hours differ by day (e.g. Mon–Fri 10–19, Sat–Sun 11–18). * * @example * ```ts * const businessHours: DaySchedule[] = [ * { day: 'monday', open: '10', close: '19' }, * { day: 'saturday', open: '11', close: '18' }, * ]; * ``` */ declare interface DaySchedule { /** Day name in lowercase: 'sunday' | 'monday' | ... | 'saturday' */ day: string; /** Opening hour (0–23) as string, e.g. '10' for 10:00 */ open: string; /** Closing hour (0–23) as string, e.g. '19' for 19:00 */ close: string; } export declare const DayView: NamedExoticComponent<DayViewProps>; /** * DayView Component * * Displays a single day's schedule with: * - Technician/artist columns on the x-axis * - Time of day on the y-axis * - Each column shows appointments for that technician * - Droppable zones for cross-technician drag-and-drop */ declare interface DayViewProps { /** The date to display */ date: Date; /** All appointments (will be filtered to this day) */ appointments: Appointment[]; /** List of technicians with id and name for columns */ technicians: Technician[]; /** Starting hour of the work day */ startHour: number; /** Ending hour of the work day */ endHour: number; /** Callback when an appointment is clicked */ onAppointmentClick?: (appointment: Appointment) => void; /** Callback when an empty slot is clicked (technicianId is passed) */ onSlotClick?: (startTime: Date, endTime: Date, technicianId?: string) => void; /** Currently selected appointment ID */ selectedAppointmentId?: string | null; /** ID of appointment being dragged */ draggingAppointmentId?: string | null; } /** * Default color used for a technician when the app does not provide one. * Uses a neutral slate that works in light and dark themes. */ export declare const DEFAULT_TECHNICIAN_COLOR = "#64748b"; /** Detail display modes */ export declare type DetailDisplayMode = 'modal' | 'panel'; export declare const DetailModal: NamedExoticComponent<DetailModalProps>; /** * DetailModal Component * * A centered modal overlay that displays appointment details with edit capability. * * Features: * - View and Edit modes * - Backdrop blur effect * - Click outside to close * - Escape key to close * - Focus trap for accessibility * - Smooth enter/exit animations */ declare interface DetailModalProps { /** The appointment to display (null when closed) */ appointment: Appointment | null; /** Whether the modal is open */ isOpen: boolean; /** Callback to close the modal */ onClose: () => void; /** Callback when appointment is updated */ onUpdate?: (appointment: Appointment) => void; /** Callback when appointment is deleted */ onDelete?: (id: string) => void; /** List of available services with id, name, and category */ services: Service[]; /** List of available technicians with id and name */ technicians?: Technician[]; /** Map of technician IDs to service IDs they can perform */ technicianServices?: TechnicianServices; } export declare const DetailPanel: NamedExoticComponent<DetailPanelProps>; /** * DetailPanel Component * * A slide-in side panel that displays appointment details with edit capability. * Alternative to the modal for users who prefer to keep context visible. * * Features: * - View and Edit modes * - Slides in from the right * - Escape key to close * - Focus trap for accessibility * - Smooth animations */ declare interface DetailPanelProps { /** The appointment to display (null when closed) */ appointment: Appointment | null; /** Whether the panel is open */ isOpen: boolean; /** Callback to close the panel */ onClose: () => void; /** Callback when appointment is updated */ onUpdate?: (appointment: Appointment) => void; /** Callback when appointment is deleted */ onDelete?: (id: string) => void; /** List of available services with id, name, and category */ services: Service[]; /** List of available technicians with id and name */ technicians?: Technician[]; /** Map of technician IDs to service IDs they can perform */ technicianServices?: TechnicianServices; } /** * Gets the end of the day (23:59:59.999) for a given date */ export declare function endOfDay(date: Date): Date; /** * Filters appointments to only those on a specific day * * @param appointments - Array of all appointments * @param date - The date to filter for * @returns Appointments that occur on the given day */ export declare function filterAppointmentsByDay(appointments: Appointment[], date: Date): Appointment[]; /** * Filters appointments to only those within working hours * Adjusts appointments that partially fall outside working hours * * @param appointments - Array of appointments * @param startHour - Working hours start * @param endHour - Working hours end * @returns Appointments within working hours */ export declare function filterByWorkingHours(appointments: Appointment[], startHour: number, endHour: number): Appointment[]; /** * Formats a Date to a full date string (e.g., "Monday, January 15, 2024") */ export declare function formatFullDate(date: Date): string; /** * Formats a Date to a short date string (e.g., "Mon 15") */ export declare function formatShortDate(date: Date): string; /** * Formats a Date to a 12-hour time string (e.g., "9:00 AM") */ export declare function formatTime(date: Date): string; /** * Generates time slots for the given hour range * Creates 30-minute intervals from startHour to endHour * * @param date - The base date for the slots * @param startHour - Starting hour (0-23) * @param endHour - Ending hour (0-23) * @returns Array of TimeSlot objects */ export declare function generateTimeSlots(date: Date, startHour: number, endHour: number): TimeSlot[]; /** * Gets a combined class string for an appointment block * * @param serviceType - The type of lash service * @param isDragging - Whether the appointment is being dragged * @returns Combined CSS class string */ export declare function getAppointmentClasses(serviceType: ServiceType, isDragging?: boolean): string; /** * Get the current theme from localStorage or system preference */ export declare function getCurrentTheme(): Theme; /** * Gets estimated duration for a service type (default values) * Used for reference when creating new appointments */ export declare function getDefaultDuration(serviceType: ServiceType): number; /** * Gets the color configuration for a service type * * @param serviceType - The type of lash service * @returns ServiceColors object with CSS class names */ export declare function getServiceColors(serviceType: ServiceType): ServiceColors; /** * Gets the display name for a service type with emoji * * @param serviceType - The type of lash service * @returns Formatted service name */ export declare function getServiceDisplayName(serviceType: ServiceType): string; /** * Resolves the color for a technician by ID. * Your app can set technician.color (e.g. hex); if missing, this default is used. * * @param technicianId - Technician/artist ID from the appointment * @param technicians - List of technicians (with optional color) * @returns CSS color string (hex or same as technician.color) */ export declare function getTechnicianColor(technicianId: string | undefined, technicians: Technician[]): string; /** * Resolves the technician color for an appointment (uses appointment.artist). */ export declare function getTechnicianColorForAppointment(appointment: Appointment, technicians: Technician[]): string; /** * Gets today's date at midnight */ export declare function getToday(): Date; /** * Gets an array of dates for the week containing the given date * Week starts on Sunday * * @param date - Any date in the desired week * @returns Array of 7 Date objects (Sun-Sat) */ export declare function getWeekDates(date: Date): Date[]; /** * Initialize theme on app load */ export declare function initializeTheme(): void; /** * Checks if two dates are on the same day */ export declare function isSameDay(date1: Date, date2: Date): boolean; /** * Checks if a date is today */ export declare function isToday(date: Date): boolean; /** * Checks if a time is within working hours */ export declare function isWithinWorkingHours(time: Date, startHour: number, endHour: number): boolean; /** * Represents a single job within an appointment. * Each job corresponds to one service performed by one technician. * * @example * ```ts * const jobs: Job[] = [ * { serviceType: 'service-1', technicianId: 'tech-1' }, * { serviceType: 'service-1', technicianId: 'tech-2' }, * { serviceType: 'service-3', technicianId: 'tech-1' }, * ]; * ``` */ export declare interface Job { /** The service ID for this job */ serviceType: ServiceType; /** The technician assigned to perform this job */ technicianId?: string; } /** * Data for creating a new appointment (without ID). * An appointment can have multiple jobs (services). */ export declare interface NewAppointmentData { client: Client; /** List of jobs (services) for this appointment */ jobs: Job[]; artist?: string; /** Status for newly created appointments (defaults to 'pending' when omitted) */ status?: AppointmentStatus; startTime: Date; duration: number; /** Optional email for client contact */ email?: string; /** Required phone number for client contact */ phone: string; notes?: string; } /** * Rounds a date to the nearest 30-minute slot */ export declare function roundToSlot(date: Date): Date; export declare function Scheduler({ appointments, technicians: providedTechnicians, services, technicianServices, startHour, endHour, businessHours, view: initialView, selectedDate: initialDate, detailDisplay, onSelectAppointment, onCreateAppointment, onNewAppointment, onUpdateAppointment, onDeleteAppointment, onRescheduleAppointment, }: SchedulerProps): JSX.Element; /** * Props for the main Scheduler component */ export declare interface SchedulerProps { /** Array of appointments to display */ appointments: Appointment[]; /** List of technicians/artists with id and name */ technicians?: Technician[]; /** List of available services (Service[] or string[]; strings are normalized to { id, name, category: 'general' }) */ services: Service[] | string[]; /** * Map of technician IDs to service IDs they can perform. * When provided, the Create Appointment modal will filter available * services based on the selected technician. * If a technician is not in the map, all services will be shown. */ technicianServices?: TechnicianServices; /** Starting hour of the work day (default: 8 for 8 AM). Ignored when businessHours is provided. */ startHour?: number; /** Ending hour of the work day (default: 21 for 9 PM). Ignored when businessHours is provided. */ endHour?: number; /** * Per-day open/close hours. When provided (non-null, non-undefined, non-empty), the grid * and slots use these hours per day instead of a single startHour/endHour. * When null, undefined, or an empty array, the whole day uses startHour/endHour. * Day names must be lowercase ('monday' … 'sunday'). */ businessHours?: DaySchedule[] | null; /** Current view mode */ view?: ViewMode; /** Currently selected/focused date */ selectedDate?: Date; /** How to display appointment details */ detailDisplay?: DetailDisplayMode; /** Callback when an appointment is clicked/selected */ onSelectAppointment?: (appointment: Appointment) => void; /** Callback when an empty slot is clicked to create new appointment (legacy) */ onCreateAppointment?: (startTime: Date, endTime: Date) => void; /** Callback when a new appointment is created with full data */ onNewAppointment?: (appointmentData: NewAppointmentData) => void; /** Callback when an appointment is updated */ onUpdateAppointment?: (appointment: Appointment) => void; /** Callback when an appointment is deleted */ onDeleteAppointment?: (id: string) => void; /** Callback when an appointment is rescheduled via drag-and-drop */ onRescheduleAppointment?: (id: string, newStartTime: Date) => void; } /** * Represents a service that can be booked. * * @example * ```ts * const services: Service[] = [ * { id: '1', name: 'Classic Lashes', category: 'lashes' }, * { id: '2', name: 'Regular Pedicure', category: { id: 1, name: 'Nail' } }, * ]; * ``` */ export declare interface Service { /** Unique identifier for the service */ id: string; /** Display name of the service */ name: string; /** Category for grouping: string (legacy) or category object with id and name */ category: string | ServiceCategory; /** Optional duration in minutes for this service */ duration?: number; } /** * Color mappings for lash service types * * - Classic: Rose/Pink - Soft and elegant * - Hybrid: Lavender/Violet - Modern and versatile * - Volume: Peach/Amber - Warm and dramatic * - Refill: Sage/Emerald - Fresh and natural */ export declare const SERVICE_COLORS: Record<ServiceType, ServiceColors>; /** Category object when provided by API */ declare interface ServiceCategory { id: number; name: string; description?: string | null; image?: string | null; updatedAt?: string; createdAt?: string; } /** * Color utility functions for the scheduler * Maps service types to consistent color schemes */ /** * Color configuration for each service type * Uses CSS class names for consistent theming */ export declare interface ServiceColors { /** CSS class name for the service type */ className: string; /** Badge background color (CSS variable) */ badgeColor: string; } /** * Core types for the Lash Studio Scheduler component */ /** Available lash service types (stores service ID) */ export declare type ServiceType = string; /** * Set the theme and persist to localStorage */ export declare function setTheme(theme: Theme): void; /** Duration of each slot in minutes */ export declare const SLOT_DURATION = 30; /** Height of each 30-minute slot in pixels */ export declare const SLOT_HEIGHT = 60; /** * Gets the start of the day (midnight) for a given date */ export declare function startOfDay(date: Date): Date; /** * Represents a technician/artist who can perform services. * * @example * ```ts * const technicians: Technician[] = [ * { id: 'tech-1', name: 'Sarah Wilson' }, * { id: 'tech-2', name: 'Emily Chen' }, * { id: 'tech-3', name: 'Jessica Rodriguez' }, * ]; * ``` */ export declare interface Technician { /** Unique identifier for the technician */ id: string; /** Display name of the technician */ name: string; /** Optional color (e.g. hex #rrggbb) for this technician; used for blocks and UI. If omitted, a default is used. */ color?: string; } /** * Map of technician IDs to the service IDs they can perform. * This allows parent applications to define which technicians are qualified * for which services. * * @example * ```ts * const technicianServices: TechnicianServices = { * 'tech-1': ['1', '2', '3'], // Technician tech-1 can do services with these IDs * 'tech-2': ['1', '4'], // Technician tech-2 can only do services 1 and 4 * 'tech-3': ['3', '5'], // Technician tech-3 specializes in services 3 and 5 * }; * ``` */ export declare type TechnicianServices = Record<string, string[]>; /** * Theme Utilities * * Helper functions for theme management */ export declare type Theme = 'light' | 'dark'; export declare function ThemeToggle({ className }: ThemeToggleProps): JSX.Element; /** * ThemeToggle Component * * A toggle button for switching between light and dark themes. * Persists the user's preference in localStorage. */ export declare interface ThemeToggleProps { /** Optional className for custom styling */ className?: string; } /** * Memoized TimeColumn to prevent unnecessary re-renders * The time column rarely changes, so memoization provides good optimization */ export declare const TimeColumn: NamedExoticComponent<TimeColumnProps>; /** * TimeColumn Component * * Renders the sticky time labels column on the left side of the scheduler. * Displays hour labels and provides visual anchors for the time grid. * * Uses position: sticky for smooth horizontal scrolling while keeping * time labels always visible. */ declare interface TimeColumnProps { /** Array of time slots to display labels for */ slots: TimeSlot[]; /** Height of each slot in pixels */ slotHeight: number; } /** * Memoized TimeGrid component */ export declare const TimeGrid: NamedExoticComponent<TimeGridProps>; /** * TimeGrid Component * * Renders the main time grid with: * - Sticky time column on the left * - Time slots as rows (30-minute intervals) * - Appointment blocks positioned absolutely within the grid * - Click handlers for empty slots to create new appointments * * This is the core layout engine for both DayView and WeekView. */ declare interface TimeGridProps { /** The date this grid represents */ date: Date; /** All appointments to potentially display */ appointments: Appointment[]; /** Starting hour of the work day */ startHour: number; /** Ending hour of the work day */ endHour: number; /** Callback when an appointment is clicked */ onAppointmentClick?: (appointment: Appointment) => void; /** Callback when an empty slot is clicked */ onSlotClick?: (startTime: Date, endTime: Date) => void; /** Currently selected appointment ID (for highlighting) */ selectedAppointmentId?: string | null; /** ID of appointment being dragged (for visual feedback) */ draggingAppointmentId?: string | null; } /** * Time slot for the grid */ export declare interface TimeSlot { /** The time for this slot */ time: Date; /** Hour component (0-23) */ hour: number; /** Minute component (0 or 30 for 30-min slots) */ minute: number; /** Formatted display string (e.g., "9:00 AM") */ label: string; /** Whether this is the start of an hour (for styling) */ isHourStart: boolean; } /** * Toggle between light and dark theme */ export declare function toggleTheme(): Theme; export declare function useDragDrop({ startHour, endHour, getHoursForDate, onReschedule, }: UseDragDropOptions): UseDragDropReturn; /** * Custom hook for managing drag-and-drop state and logic * * Provides: * - Tracking of currently dragged appointment * - Calculation of new appointment time based on drop position * - Support for cross-day rescheduling in week view * - Snap-to-slot modifier for smooth dragging */ declare interface UseDragDropOptions { /** Starting hour of the work day (for bounds checking) */ startHour: number; /** Ending hour of the work day (for bounds checking) */ endHour: number; /** When set, bounds are taken from this per-day lookup (e.g. from businessHours) */ getHoursForDate?: (date: Date) => { startHour: number; endHour: number; }; /** Callback when an appointment is successfully rescheduled */ onReschedule?: (appointmentId: string, newStartTime: Date) => void; } declare interface UseDragDropReturn { /** ID of the appointment currently being dragged */ draggingId: string | null; /** Handler for drag start events */ handleDragStart: (event: DragStartEvent) => void; /** Handler for drag end events */ handleDragEnd: (event: DragEndEvent) => void; /** Modifier to snap dragging to 30-minute slots */ snapModifier: Modifier; } export declare function useScheduler({ initialView, initialDate, detailDisplay, }?: UseSchedulerOptions): UseSchedulerReturn; /** * Main state management hook for the Scheduler component * * Manages: * - Current view mode (day/week) * - Selected date * - Selected appointment * - Detail panel/modal visibility */ declare interface UseSchedulerOptions { /** Initial view mode */ initialView?: ViewMode; /** Initial selected date */ initialDate?: Date; /** Detail display mode */ detailDisplay?: DetailDisplayMode; } declare interface UseSchedulerReturn { /** Current view mode */ view: ViewMode; /** Change the view mode */ setView: (view: ViewMode) => void; /** Currently selected date */ selectedDate: Date; /** Change the selected date */ setSelectedDate: (date: Date) => void; /** Navigate to previous day/week */ goToPrevious: () => void; /** Navigate to next day/week */ goToNext: () => void; /** Navigate to today */ goToToday: () => void; /** Currently selected appointment */ selectedAppointment: Appointment | null; /** Select an appointment (opens detail view) */ selectAppointment: (appointment: Appointment | null) => void; /** Whether the detail view is open */ isDetailOpen: boolean; /** Close the detail view */ closeDetail: () => void; /** How to display the detail */ detailDisplay: DetailDisplayMode; } /** View modes for the scheduler */ export declare type ViewMode = 'day' | 'week'; export declare const ViewToggle: NamedExoticComponent<ViewToggleProps>; /** * ViewToggle Component * * A toggle button group for switching between Day and Week views. * Styled to match the minimal, elegant aesthetic of a beauty business. */ declare interface ViewToggleProps { /** Current active view */ view: ViewMode; /** Callback when view is changed */ onViewChange: (view: ViewMode) => void; } export declare const WeekView: NamedExoticComponent<WeekViewProps>; /** * WeekView Component * * Displays a full week's schedule with: * - 7 day columns (Sunday to Saturday) * - Shared time column on the left * - Each day column shows appointments for that day * - Day headers with date and "Today" indicator * - Droppable zones for cross-day drag-and-drop * * Layout: Time column is sticky, day columns scroll horizontally if needed */ declare interface WeekViewProps { /** The date used to determine which week to display */ selectedDate: Date; /** All appointments (will be filtered per day) */ appointments: Appointment[]; /** Starting hour of the work day (union range for time column) */ startHour: number; /** Ending hour of the work day (union range for time column) */ endHour: number; /** When set, each day column uses this day's open/close; slots outside are disabled */ getHoursForDate?: (date: Date) => { startHour: number; endHour: number; }; /** Callback when an appointment is clicked */ onAppointmentClick?: (appointment: Appointment) => void; /** Callback when an empty slot is clicked */ onSlotClick?: (startTime: Date, endTime: Date) => void; /** Currently selected appointment ID */ selectedAppointmentId?: string | null; /** ID of appointment being dragged */ draggingAppointmentId?: string | null; /** Selected date range for highlighting (week view) */ selectedDateRange?: { start: Date; end: Date; } | null; /** List of technicians (used to resolve block color per technician) */ technicians?: Technician[]; } export { }