# Dependencias y vínculos entre eventos

> Dibuja vínculos fin-inicio y de otros tipos a partir de tus datos, dales estilo, reacciona a los clics y aplica las reglas de dependencia en tu propio código.

Source: https://superscheduler.org/es/docs/links-dependencies/
Reviewed: 2026-10-07

Pasa los vínculos como un array de objetos { id, from, to, type }, donde from y to son ids de eventos; FinishToStart es el tipo por defecto y las líneas curvas son el dibujo por defecto. Da estilo a los vínculos con color, width, style y cssClass o con los tokens --super-scheduler-link, y gestiona los clics con onLinkClick. La librería dibuja los vínculos pero no los hace cumplir: comprueba las dependencias en onEventMoving y onEventMove. Crear vínculos arrastrando de un evento a otro está reservado y no está implementado en 0.1.0.

Los pasos de producción, los rodajes y las campañas dependen unos de otros: el corte empieza cuando termina el diseño, dos equipos empiezan a la vez. SuperScheduler dibuja esas dependencias como vínculos entre eventos y los mantiene enganchados mientras los usuarios se desplazan, hacen zoom y arrastran. Lo que una dependencia significa para la planificación (si un movimiento está permitido, si los sucesores se desplazan) se queda en tu código, donde viven tus reglas.

Los vínculos requieren SuperScheduler Pro.

## Dibujar vínculos a partir de datos
Un vínculo es un objeto plano dentro de la prop `links`. `from` y `to` son ids de eventos (el `id` de `EventData`); `id` identifica el propio vínculo.

```ts
// src/links.ts
import type { SuperScheduler } from 'super-scheduler'

// `from` and `to` are event ids. Links between ids that are not loaded are not drawn.
export const LINKS: SuperScheduler.LinkData[] = [
  // Cutting can start once design has finished (the default type).
  { id: 'design-cut', from: 'design', to: 'cutting', type: 'FinishToStart' },
  // Painting and drying start together.
  { id: 'paint-dry', from: 'painting', to: 'drying', type: 'StartToStart', style: 'dashed' },
  // Inspection and packing finish together; drawn below the events, in a fixed color.
  {
    id: 'qa-pack',
    from: 'inspection',
    to: 'packing',
    type: 'FinishToFinish',
    color: '#7c3aed',
    layer: 'Below',
  },
  // A heavier line, styled further by CSS through its class.
  { id: 'ship', from: 'packing', to: 'shipping', width: 3, cssClass: 'link--critical' },
]

export const LINK_OPTIONS = {
  linkShape: 'Curved', // default; 'RightAngled' draws segments
  linkBottomMargin: 12, // clearance for links that run below events (8 px minimum for curves)
  linkLayer: 'Above', // default layer of links without their own `layer`
  links: LINKS,
} satisfies SuperScheduler.SchedulerConfig
```
Deberías ver cuatro líneas: una curva continua del diseño al corte, una discontinua entre la pintura y el secado, una violeta por debajo de los eventos de inspección y embalaje, y una más gruesa que llega al envío.

Los vínculos salen de tu modelo de datos: una columna `dependsOn`, una tabla de dependencias o una regla como «mismo pedido, paso siguiente». Deriva el array con `useMemo` para que su identidad cambie solo cuando cambien los datos. Desde código, `control.links` ofrece `list`, `add(link)`, `update(link)`, `remove(link or id)`, `find(id)`, `findByFromTo(from, to)` y `findAllByFromTo(from, to)`; con estado de React, es preferible actualizar tu array.

## Tipos de vínculo
| `type` | Se dibuja desde | Hasta | Significado habitual |
|---|---|---|---|
| `'FinishToStart'` (por defecto) | el final de `from` | el inicio de `to` | `to` empieza después de que termine `from` |
| `'StartToStart'` | el inicio de `from` | el inicio de `to` | los dos empiezan a la vez, o `to` después de que empiece `from` |
| `'FinishToFinish'` | el final de `from` | el final de `to` | los dos terminan a la vez |
| `'StartToFinish'` | el inicio de `from` | el final de `to` | `to` termina cuando empieza `from` |

El tipo cambia dónde se engancha la línea y cómo se dibuja. No cambia lo que los usuarios pueden hacer; consulta [hacer cumplir las dependencias](#validation).

## Forma y trazado
| Opción | Por defecto | Efecto |
|---|---|---|
| `linkShape` | `'Curved'` | Curvas suaves con un pequeño anillo en el origen y una punta de flecha. `'RightAngled'` dibuja segmentos rectos. |
| `linkBottomMargin` | `17` | Margen para los vínculos que pasan por debajo de los eventos (al menos 8 px en las curvas) |
| `linkLayer` | `'Above'` | Capa por defecto de los vínculos que no tienen su propio `layer`: por encima o por debajo de los eventos |
| `linkWidth` | `'Auto'` | Grosor de trazo por defecto en píxeles (las curvas tienen 2 px) |

Los vínculos entre extremos del mismo lado se arquean hacia fuera con un alcance limitado, de modo que los vínculos hacia filas lejanas no forman bucles desmesurados. Los vínculos hacia atrás usan el hueco entre filas y pasan por debajo de ambos eventos cuando esas filas se solapan. Los trazados esquivan los dos eventos vinculados, no todos los eventos que hay entre ellos: no es un enrutador que evite obstáculos.

## Dar estilo a los vínculos
En cada vínculo, `color`, `width`, `style` (`'solid'`, `'dashed'`, `'dotted'`), `layer` y `cssClass` cambian ese vínculo; un `width` explícito prevalece sobre el token CSS de grosor. Para el resto, usa tokens en cualquier ancestro o en una clase de vínculo:

```css
.planning {
  --super-scheduler-link: #8b5cf6;
  --super-scheduler-link-hover: #6d28d9;
  --super-scheduler-link-width: 2;
}
.dark .planning {
  --super-scheduler-link: #b89afa;
  --super-scheduler-link-hover: #d8c6ff;
}
/* A class from your data or from onBeforeLinkRender */
.link--late {
  --super-scheduler-link: var(--super-scheduler-danger);
}
```

Los vínculos son elementos SVG con la clase `super-scheduler__link` y `data-super-scheduler-part="link"`; la flecha y el anillo de origen toman el color del vínculo.

Para dar estilo según el estado de los eventos vinculados, usa `onBeforeLinkRender`. Recibe una copia del vínculo en `args.data`, y los dos eventos en `args.from` y `args.to`. Cambia `color`, `width`, `style`, `cssClass`, `layer` o `type` en la copia; los extremos no se pueden cambiar. El hook se ejecuta una vez por cada vínculo dibujado y solo vuelve a ejecutarse cuando uno de sus eventos cambia de aspecto.

## Cuándo se dibujan los vínculos
Un vínculo se dibuja cuando los dos eventos están cargados y colocados en la misma sección de la cuadrícula (las filas con scroll, o las mismas filas fijas superiores o inferiores). No se dibuja cuando:

- alguno de los dos ids no está cargado, por ejemplo porque queda fuera del rango que pediste;
- un extremo está oculto por un filtro o está en una fila plegada del árbol;
- el nivel de zoom dibuja un extremo como una barra fina de vista general;
- queda completamente fuera del área renderizada (vuelve a aparecer al desplazarse hasta él).

Mientras se arrastra un evento, sus vínculos siguen a la vista previa con el mismo trazado.

## Clic, hover y menús
- `onLinkClick` se ejecuta al hacer clic, con `args.link` (un `SuperScheduler.Link`: `data`, `id()`, `from()`, `to()`, `type()`) y `args.originalEvent`; `args.preventDefault()` omite `onLinkClicked`, que se ejecuta después.
- `linkBubble` muestra el `bubbleHtml` de un vínculo al pasar el puntero por encima.
- `contextMenuLink`, o el `contextMenu` propio de un vínculo, se abre con el clic derecho.

Los vínculos solo reciben eventos del puntero cuando alguna de estas opciones está configurada, así que por defecto nunca estorban al arrastrar eventos. Su área de clic invisible es más ancha que la línea visible.

## Hacer cumplir las dependencias en tu aplicación
Los vínculos son dibujos. Nada impide que un usuario mueva el corte antes de que termine el diseño, salvo que tu código lo compruebe. Escribe la regla una sola vez como una función normal:

```ts
// src/finishToStart.ts
import { SuperScheduler } from 'super-scheduler'

export interface Bounds {
  /** Ticks of the civil wall clock, as SuperScheduler.Date.getTime() returns them. */
  readonly start: number
  /** Exclusive end. */
  readonly end: number
}

export const boundsOf = (event: SuperScheduler.EventData): Bounds => ({
  start: new SuperScheduler.Date(event.start).getTime(),
  end: new SuperScheduler.Date(event.end).getTime(),
})

/**
 * Finish-to-start rule: a task may not start before each of its predecessors ends.
 * Returns the first link that `proposed` times for `taskId` would break, or null.
 * Other link types need rules of their own; links only draw what the data says.
 */
export function brokenFinishToStart(
  taskId: SuperScheduler.EventId,
  proposed: Bounds,
  links: readonly SuperScheduler.LinkData[],
  find: (id: SuperScheduler.EventId) => Bounds | null,
): SuperScheduler.LinkData | null {
  for (const link of links) {
    if ((link.type ?? 'FinishToStart') !== 'FinishToStart') continue
    if (link.to === taskId) {
      const predecessor = find(link.from)
      if (predecessor !== null && proposed.start < predecessor.end) return link
    } else if (link.from === taskId) {
      const successor = find(link.to)
      if (successor !== null && successor.start < proposed.end) return link
    }
  }
  return null
}

/** Ids of the links the current data already breaks, to draw them differently. */
export function lateLinks(
  links: readonly SuperScheduler.LinkData[],
  events: readonly SuperScheduler.EventData[],
): ReadonlySet<SuperScheduler.LinkData> {
  const byId = new Map(events.map((event) => [event.id, boundsOf(event)]))
  const late = new Set<SuperScheduler.LinkData>()
  for (const link of links) {
    const from = byId.get(link.from)
    const to = byId.get(link.to)
    if ((link.type ?? 'FinishToStart') === 'FinishToStart' && from && to && to.start < from.end)
      late.add(link)
  }
  return late
}
```
Después, llámala desde los hooks que se ejecutan durante un cambio y antes de aplicarlo:

```tsx
// src/ProductionPlan.tsx
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SchedulerProps, SuperScheduler } from 'super-scheduler'
import { boundsOf, brokenFinishToStart, lateLinks } from './finish-to-start'
import type { Bounds } from './finish-to-start'

export function ProductionPlan(props: {
  resources: SuperScheduler.ResourceData[]
  initialTasks: SuperScheduler.EventData[]
  /** Dependencies come from your data: the library draws them, it does not invent them. */
  initialLinks: SuperScheduler.LinkData[]
}) {
  const [tasks, setTasks] = useState(props.initialTasks)
  const [links, setLinks] = useState(props.initialLinks)
  const owned = useMemo(() => tasks.slice(), [tasks])

  // Links the data already breaks get a class; the color comes from CSS.
  const drawnLinks = useMemo(() => {
    const late = lateLinks(links, tasks)
    return links.map((link) => (late.has(link) ? { ...link, cssClass: 'link--late' } : link))
  }, [links, tasks])

  const onEventsChange = useCallback(
    (args: SchedulerEventsChangeArgs) => setTasks([...args.events]),
    [],
  )

  const config = useMemo<SchedulerProps>(() => {
    // The other tasks as the control holds them right now.
    const finder =
      (control: SuperScheduler.SchedulerApi) =>
      (id: SuperScheduler.EventId): Bounds | null => {
        const event = control.events.find(id)
        return event === null ? null : boundsOf(event.data)
      }
    return {
      linkShape: 'Curved',
      // While dragging: refuse and explain, frame by frame.
      onEventMoving(args) {
        const proposed = { start: args.start.getTime(), end: args.end.getTime() }
        const broken = brokenFinishToStart(args.e.id(), proposed, links, finder(this))
        if (broken !== null) {
          args.allowed = false
          args.message = 'This breaks a finish-to-start dependency'
        }
      },
      onEventResizing(args) {
        const proposed = { start: args.start.getTime(), end: args.end.getTime() }
        if (brokenFinishToStart(args.e.id(), proposed, links, finder(this)) !== null)
          args.allowed = false
      },
      // Before the commit: also covers keyboard moves.
      onEventMove(args) {
        const proposed = { start: args.newStart.getTime(), end: args.newEnd.getTime() }
        if (brokenFinishToStart(args.e.id(), proposed, links, finder(this)) !== null)
          args.preventDefault()
      },
      // Clicking a link offers to remove the dependency.
      onLinkClick(args) {
        const link = args.link.data
        void confirmWithUser('Remove this dependency?').then((ok) => {
          if (ok) setLinks((current) => current.filter((item) => item.id !== link.id))
        })
      },
    }
  }, [links])

  return (
    <SuperSchedulerComponent
      {...config}
      events={owned}
      links={drawnLinks}
      onEventsChange={onEventsChange}
      resources={props.resources}
      startDate="2026-10-05"
      days={14}
      scale="Day"
    />
  )
}
```
Deberías ver:

- al arrastrar un paso antes de que termine su predecesor, la tarjeta de arrastre dice «Not allowed · This breaks a finish-to-start dependency» y se rechaza la acción de soltar;
- los vínculos que tus datos ya incumplen, dibujados en el color de peligro;
- que al hacer clic en un vínculo se pregunta si quieres eliminar esa dependencia.

`onEventMoving` y `onEventResizing` se ejecutan en cada fotograma de un arrastre, así que la comprobación tiene que ser barata: busca solo los vínculos del evento arrastrado, como hace `brokenFinishToStart`. `onEventMove` se ejecuta una vez antes de confirmar el cambio y cubre también los movimientos con teclado y los eventos soltados desde fuera.

> **Tip:**
> La replanificación automática (retrasar los sucesores cuando un predecesor se alarga) también es lógica de la aplicación. En `onEventMoved` o `onEventResized`, calcula las nuevas horas de los sucesores, actualiza tu estado y agrupa todo el cambio en un [lote del historial](https://superscheduler.org/es/docs/undo-redo/#commands) para que un solo deshacer lo revierta. Los caminos críticos, los desfases y los calendarios de las dependencias no forman parte de la librería.

## Reservado: creación interactiva de vínculos
> **Limitation:**
> Crear vínculos arrastrando de un evento a otro no está implementado en 0.1.0. `linkCreateHandling`, `onLinkCreate`, `onLinkCreated`, `linkDotSize` y `linkPointSize` tienen tipos por compatibilidad, avisan una vez en desarrollo y no hacen nada; `linksLoadMethod` y `control.links.load()` no cargan desde una URL. Los campos `text` y `textAlignment` de un vínculo se aceptan, pero no se dibuja ninguna etiqueta.

Para que los usuarios creen dependencias hoy, añádelas desde tu propia interfaz (por ejemplo, con «Añadir dependencia» en el panel de detalle o en el menú contextual de un evento, eligiendo el otro evento de una lista o de la selección actual) y añade el vínculo a tu array.

## Relacionado
→ https://superscheduler.org/es/examples/manufacturing-orders/
→ https://superscheduler.org/es/examples/video-production/
→ https://superscheduler.org/es/examples/port-berths/
- [Arrastrar, redimensionar y reglas de negocio](https://superscheduler.org/es/docs/drag-resize-rules/) para `onEventMoving`, `onEventMove` y los mensajes de rechazo.
- [Deshacer, rehacer e historial](https://superscheduler.org/es/docs/undo-redo/) para que la replanificación se pueda deshacer.
