Conceptos básicosSe aplica aLite y Pro
Recursos, eventos e intervalos
Las filas son objetos ResourceData con un id y un name; las barras son objetos EventData con id, text, start, end y el id del recurso al que pertenecen. Los ids son cadenas o números que se comparan de forma estricta, así que 1 y '1' son distintos. Los intervalos son semiabiertos, [start, end), y las fechas son valores civiles de reloj escritos como cadenas ISO con segundos; la librería nunca convierte zonas horarias. Añade tus propios campos con EventData<YourFields> y acótalos con un guard cuando vuelvan del control.
SuperScheduler dibuja dos arrays: recursos, las filas, y eventos, las barras sobre esas filas. Ambos son objetos planos que creas a partir de tus propios datos. Si aciertas con cuatro reglas, evitas casi todos los problemas del tipo «mi evento no aparece»: los ids tienen un tipo estricto, los intervalos excluyen su final, las cadenas de fecha incluyen los segundos y las horas son valores de reloj sin zona horaria.
Las reglas de esta página valen para ambas ediciones salvo que una sección diga lo contrario. Lite acepta un subconjunto de los campos; consulta el inicio rápido con Lite.
Recursos
Un recurso necesita un id y un name. En Pro, ResourceData es abierto: puedes guardar tus propios campos (floor, kind, capacity) en el mismo objeto y leerlos después en los callbacks y en los hooks de render.
Los campos que más usarás en Pro:
| Campo | Para qué sirve |
|---|---|
id | Cadena o número; único entre los recursos |
name | Texto de la cabecera de fila |
backColor, cssClass, html, toolTip | Aspecto de la cabecera de fila (html es marcado de confianza) |
minHeight, eventHeight | Geometría solo de esta fila |
cellsDisabled | Todas las celdas de la fila rechazan soltar y seleccionar |
columns | Celdas para columnas adicionales de la cabecera de fila (con rowHeaderColumns) |
children, expanded | Un árbol de recursos (con treeEnabled) |
frozen | 'top' o 'bottom': la fila sigue visible durante el scroll |
Árboles de recursos
Para agrupar filas, anida recursos en children y activa treeEnabled en el scheduler. Sin treeEnabled, los hijos se ignoran y la lista sigue siendo plana. Un padre empieza plegado salvo que su campo expanded sea true. Los padres pueden contener eventos como cualquier fila; activa treePreventParentUsage para que sean solo cabeceras de grupo. Los árboles, las columnas de fila y la selección de filas se tratan en Árboles de recursos, columnas y selección. Lite solo acepta listas planas.
Eventos
Un evento necesita id, text, start, end y, para aparecer en una fila, resource. Los campos opcionales cambian su aspecto y su comportamiento:
| Campo | Para qué sirve |
|---|---|
backColor, fontColor, borderColor, barColor | Colores de la barra, su texto, su borde y su barra de duración |
cssClass | Clases para tu propio CSS |
html | Contenido como HTML de confianza (escapa los datos de usuario con SuperScheduler.Util.escapeHtml) |
toolTip, bubbleHtml | Tooltip nativo, o contenido de la burbuja al pasar el puntero |
moveDisabled, resizeDisabled | Impide mover o redimensionar este evento |
moveHDisabled, moveVDisabled | Permite moverlo solo entre filas, o solo en el tiempo |
clickDisabled, deleteDisabled | Excluye este evento de los clics o del borrado |
tags | Cualquier valor para tu propio uso |
Todos los campos de esta tabla son exclusivos de Pro salvo backColor, fontColor, cssClass, toolTip y tags, que Lite también acepta.
Los ids son cadenas o números y se comparan de forma estricta
ResourceId y EventId son string | number, y las comparaciones usan el valor y su tipo. El número 101 y la cadena '101' son ids distintos. Un evento con resource: '101' no se dibuja en una fila cuyo id es 101, y control.events.find('7') no encuentra el evento con id 7.
Esto importa sobre todo cuando los datos llegan de varias fuentes: un driver de base de datos puede devolver ids de habitación numéricos mientras un formulario o una URL dan cadenas. Normaliza los ids en el punto donde los datos entran en tu aplicación y usa un único tipo para cada clase de id.
Los intervalos son semiabiertos
Un evento ocupa [start, end): el instante de inicio le pertenece y el de fin no. Esto tiene tres consecuencias:
- Los eventos consecutivos no se solapan. Una estancia que termina a las 11:00 y la siguiente que empieza a las 11:00 en la misma habitación son compatibles, también cuando se rechazan los solapes.
- Un final con solo fecha es el primer día libre.
start: '2026-10-02',end: '2026-10-05'cubre el 2, el 3 y el 4 de octubre. Para mostrar también el 5, el final es'2026-10-06'. - Las duraciones son restas directas.
end - startes la duración, sin ajustes de «más un día».
Tu backend debería usar la misma regla. Dos intervalos se solapan cuando a.start < b.end && b.start < a.end; una consulta por rango de fechas de lo que es visible entre from y to es start < to AND end > from.
En Pro, eventEndSpec: 'Date' cambia a finales inclusivos con solo fecha para planificaciones por días: un evento que termina el '2026-10-05' cubre entonces el día 5. La librería convierte el valor internamente y lo devuelve con la misma convención. Usa una sola convención por scheduler.
Cadenas de fecha
start, end y todas las opciones de fecha aceptan un SuperScheduler.Date o una cadena ISO 8601:
| Entrada | Aceptada | Se interpreta como |
|---|---|---|
'2026-10-02' | sí | La medianoche al inicio del 2 de octubre |
'2026-10-02T14:00:00' | sí | 14:00 |
'2026-10-02 14:00:00' | sí | 14:00 (con un espacio en lugar de T) |
'2026-10-02T14:00:00.250' | sí | Con milisegundos |
'2026-10-02T14:00' | no, lanza un error | Los segundos son obligatorios |
'2026-10-02T14:00:00+02:00' | sí, con cuidado | 12:00: un desfase convierte el valor a la hora de reloj UTC |
new Date() | no | Un Date nativo no pasa la comprobación de tipos como start ni end |
Dos reglas evitan la mayoría de las sorpresas: incluye siempre los segundos y no envíes desfases ni Z salvo que quieras la hora de reloj UTC. Formatea los valores como yyyy-MM-ddTHH:mm:ss en la zona horaria del lugar que se planifica.
Hora civil, sin zonas horarias
SuperScheduler trabaja con fechas y horas civiles (de reloj): 2026-10-25T02:30:00 es «las dos y media del día 25» tal como está escrito, sin zona horaria y sin saltos por el horario de verano. El scheduler muestra lo que le das y devuelve los valores en la misma forma.
Es justo lo que necesita un tablero de planificación: un hotel en Madrid muestra la entrada a las 14:00 hora local a todos los usuarios, estén donde estén sus navegadores. También significa que las conversiones son cosa tuya:
- Si tu backend guarda instantes (marcas de tiempo UTC), conviértelos a la hora de reloj de la ubicación del recurso antes de pasarlos, y otra vez a instantes al guardar.
- Si hay recursos en zonas horarias distintas, decide qué hora de reloj muestra la vista; el scheduler tiene un único eje de tiempo.
- Los eventos periódicos (todos los lunes a las 9:00) deben llegar al scheduler como ocurrencias concretas que tu aplicación ya ha expandido.
Campos propios con EventData<T>
Tus eventos suelen llevar algo más que una etiqueta: un código de huésped, un estado, un precio. En Pro, EventData no tiene firma de índice, así que un objeto literal con propiedades adicionales no pasa la comprobación de propiedades sobrantes de TypeScript. Declara tus campos una vez y usa el genérico, SuperScheduler.EventData<YourFields>:
import type { SuperScheduler } from 'super-scheduler'
/** Fields your application adds to every booking. */
export interface BookingFields {
guestCode: string
status: 'tentative' | 'confirmed' | 'checkedIn'
adults: number
}
export type BookingEvent = SuperScheduler.EventData<BookingFields>
// ResourceData accepts extra properties: keep your own row fields next to id and name.
export const rooms: SuperScheduler.ResourceData[] = [
{ id: 101, name: 'Room 101', floor: 1, kind: 'double' },
{ id: 102, name: 'Room 102', floor: 1, kind: 'suite' },
]
// EventData has no index signature: type the array so literals may carry custom fields.
export const bookings: BookingEvent[] = [
{
id: 'bk-1042',
resource: 101, // the same type as the room id: 101 and '101' are different ids
start: '2026-10-02T14:00:00',
end: '2026-10-05T11:00:00',
text: 'Booking 1042',
guestCode: 'G-1042',
status: 'confirmed',
adults: 2,
},
]
const STATUSES: ReadonlySet<string> = new Set(['tentative', 'confirmed', 'checkedIn'])
/**
* Objects that come back from the control (handler arguments, onEventsChange) are typed as plain
* EventData. Narrow them instead of casting, so a malformed object is caught where it appears.
*/
export function isBooking(data: SuperScheduler.EventData): data is BookingEvent {
return (
'guestCode' in data &&
typeof data.guestCode === 'string' &&
'status' in data &&
typeof data.status === 'string' &&
STATUSES.has(data.status) &&
'adults' in data &&
typeof data.adults === 'number'
)
}Los datos que devuelve el control están tipados como EventData a secas: los argumentos de los handlers, onEventsChange, control.events.list. La librería conserva tus campos, pero TypeScript no puede saber que están ahí. Acota el tipo con un guard como isBooking en lugar de hacer un cast; un guard también detecta objetos que tu propio código haya construido mal. Los hooks de render reciben una copia abierta (args.data en onBeforeEventRender), así que ahí puedes leer los campos directamente, pero un guard mantiene los tipos exactos:
import { useMemo } from 'react'
import { SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'
import { bookings, isBooking, rooms } from './typed-data'
export function TypedPlanning({ onOpen }: { onOpen: (guestCode: string) => void }) {
const owned = useMemo(() => bookings.slice(), [])
const config = useMemo<SchedulerProps>(
() => ({
startDate: '2026-10-01',
days: 31,
scale: 'Day',
onBeforeEventRender: (args) => {
// args.data is a per-render copy: start and end are always SuperScheduler.Date here.
if (!isBooking(args.data)) return
const nights = Math.round(
(args.data.end.getTime() - args.data.start.getTime()) / 86_400_000,
)
// html is trusted markup: escape every value that came from users.
const code = SuperScheduler.Util.escapeHtml(args.data.guestCode)
args.data.html = `${code} · ${nights} night${nights === 1 ? '' : 's'}`
args.data.cssClass = `booking booking--${args.data.status}`
},
onEventClick: (args) => {
// args.e is a wrapper: id(), start(), text() are methods; data is the raw object.
const data = args.e.data
if (isBooking(data)) onOpen(data.guestCode)
},
}),
[onOpen],
)
return <SuperSchedulerComponent {...config} resources={rooms} events={owned} />
}Deberías ver cada reserva etiquetada con su código de huésped y su número de noches, y onOpen llamado con el código de huésped al pulsarla.
El EventData de Lite no es genérico y no acepta campos adicionales en TypeScript: guarda los datos de tu aplicación en tags, que onEventClick te devuelve en e.data.tags.
Valores después de arrastrar o redimensionar
Cuando un usuario mueve o redimensiona un evento en Pro, la librería no edita tu objeto. Lo sustituye por un objeto nuevo, { ...old, start, end, resource }, en el que start y end son instancias de SuperScheduler.Date. Los eventos que nadie ha tocado conservan las cadenas que pasaste. Por tanto, el código que lee eventos debe aceptar ambas formas:
import { SuperScheduler } from 'super-scheduler'
/**
* After a drag or a resize, the committed event holds SuperScheduler.Date values in start and end;
* events nobody touched keep the strings you passed. Normalize both to one canonical string.
*/
export function toIso(value: SuperScheduler.DateInput): string {
// `value` is `yyyy-MM-ddTHH:mm:ss` (plus `.fff` when milliseconds are not zero).
return typeof value === 'string' ? new SuperScheduler.Date(value).value : value.value
}
/** The part of an event your backend stores. */
export function toSavePayload(event: SuperScheduler.EventData) {
if (event.resource === undefined) throw new Error(`Event ${String(event.id)} has no resource`)
return {
id: String(event.id),
resource: String(event.resource),
start: toIso(event.start),
end: toIso(event.end),
}
}
/** Half-open overlap, the rule the scheduler applies: touching intervals do not overlap. */
export function overlaps(a: SuperScheduler.EventData, b: SuperScheduler.EventData): boolean {
const date = (value: SuperScheduler.DateInput) => new SuperScheduler.Date(value)
return SuperScheduler.Util.overlaps(date(a.start), date(a.end), date(b.start), date(b.end))
}Algunos detalles ayudan cuando persistes o comparas estos valores:
JSON.stringifyescribe unSuperScheduler.Datecomoyyyy-MM-ddTHH:mm:ss, así que un objeto de evento completo se serializa con cadenas ISO válidas.String(date)ydate.valuedan el mismo texto;date.toString('d MMM HH:mm', 'en-us')formatea con un patrón y un locale.- Compara fechas con
a.equals(b)o mediantegetTime().===compara la identidad de los objetos. getDay()es el día del mes (de 1 a 31), a diferencia delDatenativo. El día de la semana esgetDayOfWeek()(0 es domingo) odayOfWeekISO()(1 es lunes).- Comprueba el tipo con
value instanceof SuperScheduler.Date, nunca con el nombre del constructor. En Lite, usa elSuperScheduler.Datepropio de Lite; cuando tengas instaladas ambas ediciones, intercambia cadenas ISO entre ellas.
Planificación de una flota de alquilerUn compacto queda inmovilizado el día de la recogida. Pasa su alquiler a otro coche, respeta la limpieza y mira cuándo se queda sin flota la oficina. Reserva de instrumentos de laboratorioReserva un instrumento y la calibración viene incluida. Saca una sesión del servicio técnico y alarga tu medición.
Siguientes pasos
- Mantén los eventos en el estado de React y persiste los cambios: Eventos controlados y callbacks.
- Muestra horas y minutos en lugar de días: Horas, minutos, días y zoom.
- Personaliza las barras con tus campos: Slots de renderizado React y Temas.
Ejemplos relacionados
- FleetlinePlanificación de una flota de alquilerUn compacto queda inmovilizado el día de la recogida. Pasa su alquiler a otro coche, respeta la limpieza y mira cuándo se queda sin flota la oficina.
- BenchlabReserva de instrumentos de laboratorioReserva un instrumento y la calibración viene incluida. Saca una sesión del servicio técnico y alarga tu medición.