InteracciónSe aplica aSuperScheduler Pro
Árboles de recursos, columnas y selección
Activa treeEnabled y anida recursos en children; un padre sigue plegado salvo que tenga expanded: true. Añade columnas a la cabecera de filas con rowHeaderColumns y rellena sus celdas a partir de campos del recurso mediante display. Filtra con control.rows.filter() y control.events.filter() junto con onRowFilter y onEventFilter, y selecciona con eventClickHandling: 'Select', la selección rectangular, rowClickHandling: 'Select' y las APIs multiselect y multirange.
Las planificaciones reales rara vez tienen una lista plana de recursos. Las habitaciones pertenecen a plantas, los técnicos a regiones y las máquinas a líneas. Esta guía trata las funciones de Pro que organizan las filas y permiten trabajar con varios elementos a la vez: árboles de recursos, columnas en la cabecera de filas, filtros y los tres tipos de selección (eventos, filas y rangos de tiempo).
Todo lo que aparece aquí necesita SuperScheduler Pro (super-scheduler). La edición Lite pinta una lista plana y rechaza los children y las columns de los recursos.
Agrupar recursos en un árbol
Un árbol son datos sin más: un recurso con un array children se convierte en una fila padre. Dos cosas lo activan:
treeEnabled: trueen el scheduler. Sin esta opción, loschildrense ignoran y la cabecera de filas no tiene botones para plegar y desplegar.expanded: trueen cada padre que deba empezar abierto. Los padres están plegados salvo queexpandedsea exactamentetrue.
import { useMemo } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SuperScheduler } from 'super-scheduler'
import 'super-scheduler/styles.css'
// Regions are parent rows and technicians their children. A parent stays collapsed
// unless its resource says `expanded: true`.
const RESOURCES: SuperScheduler.ResourceData[] = [
{
id: 'north',
name: 'North region',
expanded: true,
children: [
{ id: 'ana', name: 'Ana Ruiz' },
{ id: 'ben', name: 'Ben Ortiz' },
],
},
{
id: 'south',
name: 'South region',
children: [
{ id: 'cai', name: 'Cai Moreno' },
{ id: 'dee', name: 'Dee Patel' },
],
},
]
const JOBS: SuperScheduler.EventData[] = [
{
id: 'job-1',
resource: 'ana',
start: '2026-10-05T00:00:00',
end: '2026-10-07T00:00:00',
text: 'Panel upgrade',
},
{
id: 'job-2',
resource: 'cai',
start: '2026-10-06T00:00:00',
end: '2026-10-09T00:00:00',
text: 'Boiler service',
},
]
export function TeamPlanner() {
const { controlRef, control } = useSchedulerControl()
// The control adopts the events array and mutates it: hand it a copy.
const events = useMemo(() => JOBS.slice(), [])
return (
<section aria-labelledby="team-plan-title">
<h2 id="team-plan-title">Field teams</h2>
<div role="toolbar" aria-label="Rows">
<button type="button" disabled={control === null} onClick={() => control?.rows.expandAll()}>
Expand all
</button>
<button
type="button"
disabled={control === null}
onClick={() => control?.rows.collapseAll()}
>
Collapse all
</button>
</div>
<SuperSchedulerComponent
controlRef={controlRef}
startDate="2026-10-05"
days={14}
scale="Day"
cellWidth={56}
treeEnabled
treeIndent={16}
// Region rows group technicians: no jobs and no time selection on them.
treePreventParentUsage
resources={RESOURCES}
events={events}
/>
</section>
)
}Deberías ver «North region» abierta con sus dos técnicos, «South region» cerrada y los dos botones funcionando en cuanto el scheduler se haya montado (hasta entonces, control es null).
Opciones relacionadas:
| Opción | Por defecto | Efecto |
|---|---|---|
treeEnabled | false | Construye las filas a partir de children. |
treeIndent | 20 | Sangría por nivel, en píxeles. |
treePreventParentUsage | false | Las filas padre no aceptan eventos ni selección de tiempo. |
treeAutoExpand | true | Un padre plegado se abre cuando se arrastra un evento sobre él durante medio segundo. |
rowFilterParentsAlwaysVisible | true | Mantiene los ancestros de una fila que cumple un filtro. |
Los padres también pueden contener eventos (una reserva para «todo el equipo») salvo que actives treePreventParentUsage. Un recurso concreto rechaza los eventos que se sueltan sobre él con preventUsage: true, y los recursos de primer nivel se pueden fijar por encima o por debajo de las filas con scroll con frozen: 'top' o frozen: 'bottom', algo útil para filas de resumen.
Desplegar y plegar desde código
El control expone el árbol mediante control.rows:
control.rows.expandAll()ycontrol.rows.collapseAll();control.rows.expand(level)abre los padres por encima de un nivel (1por defecto,-1para todos los niveles);control.rows.find(id)devuelve una fila;row.expand(),row.collapse()yrow.toggle()la cambian.
onResourceExpand y onResourceCollapse comunican un cambio después de que ocurra, con la fila como args.resource. Úsalos para recordar qué ha abierto el usuario o para mantener sincronizadas otras partes de tu interfaz.
Añadir columnas a la cabecera de filas
Sin rowHeaderColumns, la cabecera de filas muestra el name de cada recurso. Con esta opción, la cabecera se convierte en una pequeña tabla:
textes el título de la columna (también se aceptannameytitle);displaynombra un campo del recurso cuyo valor rellena la celda (antes se busca un campo con ese nombre dentro detags);- la primera columna muestra el
namedel recurso cuando no tienedisplay; widthfija el ancho en píxeles (por defecto,rowHeaderColumnDefaultWidth, 80);sorthace que el título se pueda pulsar: ordena las filas por ese campo, primero de forma ascendente y después descendente, y ponearia-sorten el título.
Un recurso también puede llevar sus propias columns: [{ text }, { html }]. Esas entradas se asignan por posición, una por columna.
import { useState } from 'react'
import { type SuperScheduler, SuperSchedulerComponent } from 'super-scheduler'
import 'super-scheduler/styles.css'
type Status = 'available' | 'off'
const TECHNICIANS: SuperScheduler.ResourceData[] = [
{ id: 'ana', name: 'Ana Ruiz', skill: 'Electrical', shift: 'Early', status: 'available' },
{ id: 'ben', name: 'Ben Ortiz', skill: 'HVAC', shift: 'Late', status: 'off' },
{ id: 'cai', name: 'Cai Moreno', skill: 'Plumbing', shift: 'Early', status: 'available' },
]
// `display` reads a resource field, so a column keeps its values when it is reordered.
// `sort` makes the title clickable: it sorts the rows by that field, ascending then descending.
const INITIAL_COLUMNS: SuperScheduler.RowHeaderColumnData[] = [
{ text: 'Technician', width: 150, sort: 'name' },
{ text: 'Skill', display: 'skill', width: 110, sort: 'skill' },
{ text: 'Shift', display: 'shift', width: 70, nonresizable: true },
{ text: 'Status', display: 'status', width: 100 },
]
const STATUS_LABEL: Record<Status, string> = { available: 'Available', off: 'Day off' }
// Runs with `this` = the control, whose rowHeaderColumns are always in the current order.
function decorateStatus(
this: SuperScheduler.SchedulerApi,
args: SuperScheduler.SchedulerBeforeRowHeaderRenderArgs,
): void {
const index = this.rowHeaderColumns?.findIndex((column) => column.display === 'status') ?? -1
const cell = args.row.columns[index]
const status = args.row.data.status
if (cell === undefined || (status !== 'available' && status !== 'off')) return
// With rowHeaderColumns, row.html is ignored: write the column's own html (trusted markup).
cell.html = `<span class="tech-status tech-status--${status}">${STATUS_LABEL[status]}</span>`
}
export function TechnicianColumns({ events }: { events: SuperScheduler.EventData[] }) {
// Kept in state so a parent re-render never hands the control the original order again.
const [columns, setColumns] = useState(INITIAL_COLUMNS)
return (
<SuperSchedulerComponent
startDate="2026-10-05"
days={14}
scale="Day"
cellWidth={56}
resources={TECHNICIANS}
events={events}
rowHeaderColumns={columns}
rowHeaderColumnsReorderable
onRowHeaderColumnsChange={(args) => setColumns([...args.columns])}
onBeforeRowHeaderRender={decorateStatus}
/>
)
}Para decorar una celda, usa onBeforeRowHeaderRender. Con columnas, el html propio de la fila se ignora: escribe en args.row.columns[i].html. Ese marcado es HTML de confianza, así que escapa el texto de los usuarios con SuperScheduler.Util.escapeHtml. Para contenido React en la cabecera de filas, consulta Slots de renderizado React.
Redimensionar y reordenar columnas
Las columnas se pueden redimensionar por defecto (rowHeaderColumnsResizable: true); nonresizable: true lo desactiva para una columna. Tras redimensionar, el control escribe el nuevo ancho en ese objeto de columna de tu array rowHeaderColumns y llama a onRowHeaderColumnResized({ column }).
La reordenación está desactivada por defecto. Con rowHeaderColumnsReorderable, los usuarios arrastran los títulos de las columnas; los bordes de redimensionado tienen prioridad sobre el arrastre. En keyboardMode: 'Full', Alt+Mayús+flecha izquierda y Alt+Mayús+flecha derecha mueven el título con foco y anuncian el cambio. Desde código, control.moveRowHeaderColumn(from, to) mueve una columna según su índice en el orden actual.
Cada redimensionado y cada reordenación llaman a onRowHeaderColumnsChange con:
columns: las columnas en su nuevo orden, con sus anchos;order:order[i]es el índice original (en la prop que pasaste) de la columna que ahora está en la posicióni;widthsyreason('resize'o'reorder').
Guarda las columnas en el estado de React, como hace el snippet, y adopta args.columns. De lo contrario, un padre que se vuelva a renderizar con un array literal nuevo devolvería el orden original al control.
Filtrar filas y eventos
Un filtro es un parámetro más un callback. control.rows.filter(param) guarda el parámetro y llama a onRowFilter para cada fila; pon args.visible = false para ocultarla. control.events.filter(param) y onEventFilter funcionan igual con los eventos. Un parámetro falsy ('', null, 0) borra el filtro.
import { useEffect, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
type JobStatus = 'scheduled' | 'done' | 'cancelled'
type Job = { status: JobStatus }
// Module-level handlers keep their identity: a new onRowFilter or onEventFilter
// function makes the control evaluate the active filter again.
const FILTERS: Pick<SchedulerProps, 'onRowFilter' | 'onEventFilter'> = {
onRowFilter: (args) => {
const query = String(args.filterParam).toLowerCase()
args.visible = args.row.name.toLowerCase().includes(query)
},
onEventFilter: (args) => {
const job = args.e.data as SuperScheduler.EventData<Job>
args.visible = job.status === args.filterParam
},
}
export function FilteredJobs(props: {
resources: SuperScheduler.ResourceData[]
jobs: SuperScheduler.EventData<Job>[]
}) {
const { controlRef, control } = useSchedulerControl()
const [query, setQuery] = useState('')
const [status, setStatus] = useState<JobStatus | ''>('')
const events = useMemo(() => props.jobs.slice(), [props.jobs])
// A falsy parameter ('' or null) clears the filter.
useEffect(() => control?.rows.filter(query.trim() || null), [control, query])
useEffect(() => control?.events.filter(status || null), [control, status])
return (
<>
<label>
Technician <input type="search" value={query} onChange={(e) => setQuery(e.target.value)} />
</label>
<label>
Status
<select value={status} onChange={(e) => setStatus(e.target.value as JobStatus | '')}>
<option value="">All</option>
<option value="scheduled">Scheduled</option>
<option value="done">Done</option>
<option value="cancelled">Cancelled</option>
</select>
</label>
<SuperSchedulerComponent
{...FILTERS}
controlRef={controlRef}
startDate="2026-10-05"
days={14}
scale="Day"
treeEnabled
// Keeps the region row of every matching technician (default true).
rowFilterParentsAlwaysVisible
resources={props.resources}
events={events}
/>
</>
)
}Al escribir «ben» quedan «North region» y «Ben Ortiz»: rowFilterParentsAlwaysVisible mantiene los ancestros de cada fila que coincide. Elegir un estado oculta los demás eventos sin tocar las filas.
Los filtros se ejecutan en el navegador sobre los datos que tiene el control. Una búsqueda en datos que no están cargados (otro mes, otra sede) es una consulta a tu backend, seguida de nuevos resources o events.
Seleccionar eventos
Por defecto, un clic en un evento solo ejecuta onEventClick. La selección necesita eventClickHandling: 'Select':
- un clic alterna la selección del evento y deselecciona los demás;
- Ctrl+clic (Cmd+clic en macOS) alterna su selección y mantiene los demás, mientras
allowMultiSelectseatrue(el valor por defecto); - con
eventMultiSelectRange, Mayús+clic selecciona los eventos entre el último evento pulsado y este, a lo largo de las filas visibles; Ctrl/Cmd+Mayús los añade.
onEventSelect se ejecuta antes del cambio y puede cancelarlo con args.preventDefault(); onEventSelected se ejecuta después.
La API control.multiselect lee y cambia la selección: get(), add(e), remove(e), clear(), isSelected(e) y selectAll({ scope, filter }). selectAll selecciona por defecto los eventos visibles; scope: 'all' recorre todos los eventos cargados, con un coste proporcional al tamaño del almacén.
import { useEffect, useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
import { subscribeScheduler } from 'super-scheduler/hooks'
type Job = { status: 'scheduled' | 'done' }
export function SelectableJobs(props: {
resources: SuperScheduler.ResourceData[]
jobs: SuperScheduler.EventData<Job>[]
onReschedule: (ids: SuperScheduler.EventId[]) => void
}) {
const { controlRef, control } = useSchedulerControl()
const [count, setCount] = useState(0)
const events = useMemo(() => props.jobs.slice(), [props.jobs])
// Rectangle selections and multiselect calls (selectAll, clear) publish the
// 'selection' topic; clicks are reported by onEventSelected below.
useEffect(() => {
if (control === null) return
return subscribeScheduler(control, 'selection', (selected) => setCount(selected.length))
}, [control])
const config = useMemo<SchedulerProps>(
() => ({
// A click toggles the event; Ctrl+click (Cmd+click on macOS) keeps the others.
eventClickHandling: 'Select',
// Shift+click selects every event between the last clicked one and this one.
eventMultiSelectRange: true,
// Shift+drag over the grid selects the events inside the rectangle.
rectangleSelectHandling: 'EventSelect',
// With the rectangle: Shift adds to the selection, Alt removes from it.
rectangleSelectModifiers: true,
onEventSelected() {
setCount(this.multiselect.get().length)
},
// Marks selected events for CSS. The hook runs again for every event whose
// selection changes.
onBeforeEventRender: (args) => {
const e = args.control.events.find(args.data.id)
if (e !== null && args.control.multiselect.isSelected(e)) {
args.data.cssClass = [args.data.cssClass, 'is-selected'].filter(Boolean).join(' ')
}
},
}),
[],
)
const selectScheduled = () =>
control?.multiselect.selectAll({
scope: 'view',
filter: (e) => (e.data as SuperScheduler.EventData<Job>).status === 'scheduled',
})
const reschedule = () => {
const ids = control?.multiselect.get().map((e) => e.id()) ?? []
if (ids.length > 0) props.onReschedule(ids)
}
return (
<>
<div role="toolbar" aria-label="Selection">
<button type="button" onClick={selectScheduled}>
Select scheduled in view
</button>
<button type="button" onClick={() => control?.multiselect.clear()}>
Clear
</button>
<button type="button" disabled={count === 0} onClick={reschedule}>
Reschedule {count} {count === 1 ? 'job' : 'jobs'}
</button>
</div>
<SuperSchedulerComponent
{...config}
controlRef={controlRef}
startDate="2026-10-05"
days={14}
scale="Day"
resources={props.resources}
events={events}
/>
</>
)
}Deberías ver cómo el contador del botón sigue los clics, los rectángulos y las acciones de la barra de herramientas, y los trabajos seleccionados resaltados con este CSS:
.super-scheduler__event.is-selected {
z-index: 2;
box-shadow: 0 0 0 2px var(--super-scheduler-accent), var(--super-scheduler-shadow-2);
}Con allowMultiMove, arrastrar un evento seleccionado mueve toda la selección; cada evento pasa por las mismas reglas de movimiento (consulta arrastrar, redimensionar y reglas de negocio). En el modo de teclado completo, Ctrl/Cmd+A selecciona todos los eventos visibles.
Selección rectangular
rectangleSelectHandling decide qué hace un rectángulo trazado con el puntero:
'EventSelect'selecciona los eventos que contiene (y ejecuta los callbacks);'Enabled'solo ejecuta los callbacks, para aplicaciones que actúan por su cuenta sobre el rectángulo;'Disabled'(por defecto) lo desactiva.
Los usuarios empiezan un rectángulo con Mayús + arrastrar sobre la cuadrícula. Para empezarlo sin Mayús, pon args.action = 'RectangleSelect' en onGridMouseDown, o llama a control.multiselect.startRectangle() antes de la siguiente pulsación. rectangleSelectMode: 'Row' mantiene el rectángulo en la fila donde empezó la pulsación. Con rectangleSelectModifiers, Mayús añade a la selección actual y Alt quita de ella; sin ninguna de las dos teclas, el rectángulo sustituye la selección.
Los callbacks son onRectangleSelecting (en cada fotograma; args.visible = false oculta el rectángulo), onRectangleSelect (cancelable, con events, start, end y resources) y onRectangleSelected.
Seleccionar filas
rowClickHandling: 'Select' hace que un clic en la cabecera de fila seleccione la fila. Ctrl/Cmd alterna la selección de una fila y Mayús selecciona un rango desde la última fila pulsada. Las filas seleccionadas llevan aria-selected="true".
import { useMemo, useState } from 'react'
import { SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps, SuperScheduler } from 'super-scheduler'
export function RosterRows(props: {
resources: SuperScheduler.ResourceData[]
events: SuperScheduler.EventData[]
onNotify: (resourceIds: SuperScheduler.ResourceId[]) => void
}) {
const { controlRef, control } = useSchedulerControl()
const [selected, setSelected] = useState<readonly SuperScheduler.ResourceId[]>([])
const events = useMemo(() => props.events.slice(), [props.events])
const config = useMemo<SchedulerProps>(
() => ({
// A click on a row header selects the row; Ctrl/Cmd toggles, Shift selects a range.
rowClickHandling: 'Select',
onRowSelected() {
setSelected(this.rows.selection.get().map((row) => row.id))
},
}),
[],
)
return (
<>
<button
type="button"
disabled={selected.length === 0}
onClick={() => props.onNotify([...selected])}
>
Notify {selected.length} technicians
</button>
<button
type="button"
onClick={() => {
control?.rows.selection.clear()
setSelected([])
}}
>
Clear rows
</button>
<SuperSchedulerComponent
{...config}
controlRef={controlRef}
startDate="2026-10-05"
days={14}
scale="Day"
resources={props.resources}
events={events}
/>
</>
)
}control.rows.selection ofrece get(), add(row), remove(row), isSelected(row) y clear(); estas llamadas resaltan las filas al instante y no disparan ningún callback, y por eso el snippet restablece su propio estado después de clear(). onRowSelect (cancelable) y onRowSelected se ejecutan alrededor de cada selección hecha con un clic.
La prop selectedRows contiene los ids de los recursos seleccionados. Pásala para empezar con una selección; el control actualiza ese mismo array a medida que cambia la selección.
Seleccionar rangos de tiempo
Arrastrar sobre celdas vacías selecciona un rango de tiempo y llama a onTimeRangeSelected una vez, al soltar, con start, end (exclusivo), resource y origin: 'drag'. Otros orígenes:
'click': un clic simple en una celda vacía selecciona esa celda;'keyboard': con el teclado activado, Intro sobre una celda con foco, o Mayús+flecha izquierda/derecha para ampliar un rango;'api':control.selectTimeRange(start, end, resource).
Comprueba args.origin cuando solo un arrastre deba crear algo. La selección sigue dibujada después de soltar, hasta la siguiente selección o hasta control.clearSelection(). Un clic en otro sitio también la borra, salvo con timeRangeSelectedHandling: 'HoldForever'; 'Disabled' desactiva la selección de rangos.
onTimeRangeSelecting se ejecuta en cada cambio durante el arrastre. Pon args.allowed = false para rechazar un rango (la sombra se marca y al soltar no se selecciona nada), o reescribe args.start y args.end para acotarlo. Las celdas deshabilitadas y los padres bajo treePreventParentUsage rechazan las selecciones sin necesidad de código.
Varios rangos a la vez
Con allowMultiRange, un nuevo arrastre puede añadir un rango en lugar de sustituir el actual: mientras se mantiene pulsado Ctrl/Cmd (multiRangeMode: 'CtrlOrMeta', el valor por defecto) o siempre ('Always'). args.multirange enumera todos los rangos seleccionados, y control.multirange los lee o los cambia: get(), clear() y add(new SuperScheduler.Selection(start, end, resource)).
import { useMemo, useState } from 'react'
import { SuperScheduler, SuperSchedulerComponent, useSchedulerControl } from 'super-scheduler'
import type { SchedulerProps } from 'super-scheduler'
export interface BlockRequest {
readonly resource: SuperScheduler.ResourceId
/** ISO wall-clock values; `end` is exclusive. */
readonly start: string
readonly end: string
}
export function RangeBlocker(props: {
resources: SuperScheduler.ResourceData[]
events: SuperScheduler.EventData[]
onBlock: (requests: BlockRequest[]) => void
}) {
const { controlRef, control } = useSchedulerControl()
const [ranges, setRanges] = useState<readonly SuperScheduler.SchedulerRange[]>([])
const events = useMemo(() => props.events.slice(), [props.events])
const config = useMemo<SchedulerProps>(
() => ({
// Ctrl/Cmd + drag adds a range instead of replacing the current one.
allowMultiRange: true,
multiRangeMode: 'CtrlOrMeta',
onTimeRangeSelecting: (args) => {
// Past days cannot be blocked: the shadow turns red and the drop is refused.
args.allowed = args.start.getTime() >= SuperScheduler.Date.today().getTime()
},
onTimeRangeSelected: (args) => {
// A plain click on an empty cell also selects it (origin 'click').
setRanges([...args.multirange])
},
}),
[],
)
const block = () => {
props.onBlock(
ranges.map((range) => ({
resource: range.resource,
start: range.start.value,
end: range.end.value,
})),
)
control?.clearSelection()
setRanges([])
}
return (
<>
<button type="button" disabled={ranges.length === 0} onClick={block}>
Block {ranges.length} selected {ranges.length === 1 ? 'range' : 'ranges'}
</button>
<SuperSchedulerComponent
{...config}
controlRef={controlRef}
startDate="2026-10-05"
days={14}
scale="Day"
resources={props.resources}
events={events}
/>
</>
)
}Arrastra sobre días libres de una habitación y después de otra manteniendo pulsado Ctrl (Cmd en macOS): el botón muestra «Block 2 selected ranges». Los rangos llegan a onBlock como valores ISO de reloj con finales exclusivos, listos para enviarlos a tu backend.
De qué es dueña tu aplicación
La librería mantiene los árboles, los filtros y las selecciones en el navegador, durante la sesión actual. Tu aplicación decide:
- de dónde sale el árbol: las filas son los
resourcesque pasas, y el control nunca pide hijos por su cuenta (onLoadNodeestá reservado); - qué significa un filtro cuando los datos no están cargados, y cómo llega la búsqueda al backend;
- qué hace una selección: las acciones en bloque, los permisos para ejecutarlas y su persistencia;
- si las filas desplegadas y la disposición de las columnas sobreviven a una recarga (consulta las vistas guardadas).
Las selecciones no se guardan en ningún sitio. El estado de despliegue vive en los objetos de recurso que pasaste hasta que los sustituyas.
Relacionado
Planning de habitaciones de hotelGotea la ducha de la 104. Recoloca al siguiente cliente, bloquea la habitación para el fontanero y localiza las noches que ya están llenas. Planificación de aulas de formaciónLa matrícula superó el aula. Selecciona ambas sesiones, mira qué está libre para las dos, muévelas juntas y conserva la vista. Despacho de servicio técnicoEntra un trabajo urgente en la cola. Encuentra la cuadrilla que puede atenderlo a tiempo.
- Teclado, accesibilidad y táctil enumera las teclas de selección.
- Eventos controlados y callbacks explica cómo llegan los eventos seleccionados a tu estado.
- Paneles coordinados y vistas guardadas conserva las filas plegadas y los anchos de columna.
Ejemplos relacionados
- Casa NomaPlanning de habitaciones de hotelGotea la ducha de la 104. Recoloca al siguiente cliente, bloquea la habitación para el fontanero y localiza las noches que ya están llenas.
- MorrowPlanificación de aulas de formaciónLa matrícula superó el aula. Selecciona ambas sesiones, mira qué está libre para las dos, muévelas juntas y conserva la vista.
- FieldworkDespacho de servicio técnicoEntra un trabajo urgente en la cola. Encuentra la cuadrilla que puede atenderlo a tiempo.
- FramehousePlanificación de producción audiovisualUn rodaje se alarga. Mueve la edición que dependía de él, entiende por qué y vuelve atrás.