# Pantalla de consumos adicionales — especificación

> #24979. **Solo especificación: no hay código de pantalla.** `web/src/app/ordenes/[id]/page.tsx`
> es de Christian y está en movimiento; esto se implementa cuando conteste #24978 (preguntas 1 y 2)
> y se sepa si esa pantalla cambia. El contrato de API que la sostiene YA está implementado y
> probado por HTTP (`api/tests/Integracion/ConsumoAdicionalHttpTest.php`).

## Dónde va y cuánto toca lo de Christian

Un componente **nuevo y autocontenido**, `web/src/components/consumos-adicionales.tsx`, que recibe
`{ orden: Orden; onCambio: () => void }` y hace sus propias llamadas. En `page.tsx` entra con **una
línea** (`<ConsumosAdicionales orden={orden} onCambio={cargar} />`, entre «Insumos» y los documentos).
Ese montaje es lo único que puede chocar; el resto es archivo nuevo. Usa lo que la página ya usa:
`api()` de `lib/api.ts`, `Dialog`, `Table`, `Badge`, `toast` de sonner, `numero` y `leerNombre`.

## Cuándo se muestra

| estado de la orden | qué se ve |
|---|---|
| `BORRADOR`, `PLANIFICADA` | nada (todavía no hay despacho; los consumos adicionales no existen) |
| `MP_DESPACHADA` | la tarjeta completa, con acciones |
| `PRODUCIDA`, `TERMINADA`, `ANULADA` | la tarjeta **solo lectura**, y solo si hubo consumos (si no, no estorba) |

Permisos (los mismos nombres que ya usa la pantalla; el servidor vuelve a verificar cada uno):
**pedir, anular y reasignar** → `registrar_produccion`; **despachar** → `despachar_mp`; **ver** → `ver`.
Esconder un botón no protege: es solo para no ofrecer lo que va a dar 403.

## Contrato de API (ya existe)

Todas contestan `{ error: false, datos: { orden, estado, consumos: [...] } }`, y los errores de regla
`{ error: true, mensaje }` con un texto ya redactado para la persona (se muestra entero, como el resto).

| acción | llamada | cuerpo |
|---|---|---|
| listar | `GET /api/ordenes/{id}/consumos-adicionales` | — |
| pedir | `POST …/consumos-adicionales` | `{ pluProducto, pluInsumo, cantidad, motivo, requiereDespacho }` |
| despachar lo pendiente | `POST …/consumos-adicionales/despachar` | — |
| anular | `POST …/consumos-adicionales/{consumoId}/anular` | `{ motivo }` |
| cambiar de producto | `POST …/consumos-adicionales/{consumoId}/reasignar` | `{ pluProducto }` |

Tipo a agregar en `lib/api.ts` (hoy no existe): `ConsumoAdicional = { id, pluProducto, pluInsumo,
descripcionInsumo, cantidad, motivo, requiereDespacho, estado: "PENDIENTE"|"DESPACHADO"|"CONSUMIDO"|"ANULADO",
despachoDocumento, consumoDocumento, existenciaAlAgregar, hayFaltante, creadoPor, creadoEn }`.

## La tarjeta

Título: **«Materia prima adicional»**. Subtítulo: *«Lo que la planta gastó por encima de la fórmula
para completar el lote. Se suma al costo del producto.»*

Tabla: **Producto** (al que se le carga) · **Materia prima** · **Cantidad** · **Motivo** · **Estado** ·
acciones. El estado es un `Badge`: Pendiente (ámbar) → «Falta traerla a producción»; Despachado (azul) →
«En producción, se gasta al registrar»; Consumido (verde) → «Gastada, doc. {consumoDocumento}»;
Anulado (gris, tachado).

Si `hayFaltante`: aviso *«En la bodega de materia prima hay {existenciaAlAgregar}; se piden {cantidad}.
El despacho se puede hacer igual (la bodega física manda sobre el saldo del sistema).»* — **avisa, no frena**
(decisión de Christian del 24-sep).

### Acciones

1. **«Agregar materia prima»** (abre diálogo): producto (`Select`, solo los de la orden; preseleccionado si
   hay uno), materia prima (buscador con `/api/catalogo/productos`, el mismo que usa «nueva orden»),
   cantidad (`Input number`, `step=any`, `min=0`), motivo (`Textarea`, obligatorio, 255 máx., con
   sugerencias de un clic: «Completar lote», «Merma de arranque»), y un `Checkbox`
   **«Ya está en la bodega de producción (sobrante)»** → `requiereDespacho=false`.
2. **«Despachar lo pendiente»** — visible solo si hay `PENDIENTE`; confirma con el texto *«Se hará UN
   traslado de {bodega MP} a {bodega producción} con {n} renglones.»*. Después de éxito, `onCambio()`.
3. **Anular** por fila, solo en `PENDIENTE`, pide motivo (mismo patrón que `DialogoAnular`).
4. **Cambiar producto** por fila, solo si la orden tiene más de un producto y la fila no está cerrada.

### Coordinación con el botón «Registrar» (el que ya existe)

- Si hay consumos `PENDIENTE`, el botón «Registrar» se muestra **deshabilitado** con la explicación
  *«Hay {n} materias primas adicionales sin despachar. Despachalas o anulalas antes de registrar.»* —
  el servidor igual lo rechaza, pero la pantalla no debe dejar llegar al error.
- El texto del diálogo de producción debe agregar: *«Se gastará además la materia prima adicional
  listada arriba ({n} renglones).»*. Esto sí toca el componente de Christian: **va en la misma
  integración, no antes**.

## Casos que la pantalla tiene que cubrir

- Producto con producción en cero y consumos cargados → el servidor rechaza al registrar con un mensaje
  que dice qué hacer (reasignar); la pantalla lo muestra con `duration: 10000`, como el resto.
- Dos personas a la vez: el segundo «Despachar» recibe *«No hay consumos pendientes… no se despacha dos
  veces»* → recargar la lista, no mostrarlo como falla.
- Teléfono (375 px): la tabla se vuelve lista de tarjetas (el módulo ya se verificó sin desplazamiento
  horizontal; no romperlo).

## Cómo se prueba (cuando se implemente)

No hay marco de pruebas de pantalla en `web/` (`package.json` solo tiene `build` y `lint`), así que:
`npm run build` + `npm run lint` limpios, y el recorrido por navegador contra la base local con el esquema
real: pedir → ver el faltante → despachar → registrar → comprobar el costo del producto terminado →
trasladar; más una pasada a 375 px. La lógica de negocio ya está cubierta por las pruebas del backend.

## Decisión de producto abierta (⏳ #24978 pregunta 2)

¿El motivo es texto libre o una lista cerrada que Envaisa mantenga? Se especifica libre + sugerencias;
cambiarlo a lista es una columna y un `Select`.
