314 lines
47 KiB
Markdown
314 lines
47 KiB
Markdown
# Capítulos en cotizaciones: auditoría y plan
|
|
|
|
Fecha: 19 de octubre de 2026. Estado: fases 1 a 5 implementadas y probadas sin conexión a base de datos, incluida generación real de PDF con Chromium. La aplicación y validación del esquema en MariaDB siguen pendientes de un entorno autorizado; todavía no se considera completada la funcionalidad de extremo a extremo.
|
|
|
|
## Requisitos confirmados
|
|
|
|
- Las cotizaciones nuevas comienzan sin capítulos. Las antiguas siguen pudiéndose consultar, editar, duplicar y exportar sin convertirlas.
|
|
- Una cotización puede tener cero capítulos o uno o más capítulos. Si tiene capítulos, todos sus detalles pertenecen a uno de ellos, incluidos los bloques de texto.
|
|
- El capítulo tiene una cabecera donde solo se edita su descripción y un pie con su subtotal automático, sin campos editables.
|
|
- Un botón en la barra de herramientas de detalles permite añadir capítulos.
|
|
- Al mover un capítulo se desplazan todos sus detalles, conservando el orden interior.
|
|
- El PDF soporta ambos modos. Con capítulos se puede mostrar el desglose económico de los detalles o mostrar únicamente sus totales de capítulo, conservando las descripciones.
|
|
|
|
## Fase 1: decisiones funcionales
|
|
|
|
Las reglas de esta sección y de los apartados «Comportamiento del editor» e «Informe PDF» constituyen la especificación funcional de partida para las fases 2 a 6. Se concretan los valores propuestos en el plan de actuación; cualquier cambio posterior debe actualizar sus casos de aceptación.
|
|
|
|
### Modos, títulos e importes
|
|
|
|
- La cotización empieza en modo libre, sin capítulos. Leer, editar o exportar una cotización antigua no activa la agrupación.
|
|
- Crear el primer capítulo cambia toda la cotización al modo agrupado en una única operación: todos sus detalles, incluidos los textos, pasan al capítulo. No existen estados intermedios guardables con detalles huérfanos.
|
|
- Se permiten capítulos vacíos y capítulos que solo contienen texto. Ambos muestran subtotal de cero en la moneda de la cotización.
|
|
- La descripción del capítulo es obligatoria: se rechaza un valor vacío o compuesto solo por espacios al crear o guardar. Puede coincidir con la de otro capítulo; la identidad no depende del título. Cancelar el alta no modifica los detalles ni el modo.
|
|
- Los capítulos se numeran automáticamente desde 1 según su orden. La cabecera muestra «1. Descripción» y el pie «Subtotal capítulo 1. Descripción», con sus etiquetas traducidas. La numeración no es editable ni forma parte de la descripción guardada.
|
|
- La cabecera solo permite editar la descripción. Sus botones de selección, movimiento y acciones no representan campos de datos editables. El subtotal se muestra en el pie y no se repite como campo monetario editable en la cabecera.
|
|
- No se incluyen capítulos anidados. El plegado visual del editor se incorpora por petición posterior: los capítulos empiezan desplegados; el botón de flecha oculta sus detalles y acciones, conservando título y subtotal visibles. Este estado local se mantiene al reordenar el capítulo y no se guarda ni se envía al PDF. Los capítulos duplicados empiezan desplegados.
|
|
- El subtotal suma los totales de sus líneas tras sus descuentos individuales y antes del descuento general e impuestos. No hay descuentos, impuestos ni moneda propios del capítulo. Se conservan las reglas actuales de precisión y redondeo.
|
|
- Agrupar, desagrupar o mover contenido conserva los valores de las líneas y el total general. La visibilidad del PDF tampoco modifica importes ni los oculta en el editor.
|
|
|
|
### Capítulo activo y destinos
|
|
|
|
- Crear un capítulo, activar su cabecera o interactuar con uno de sus detalles lo convierte en el capítulo activo. La selección múltiple no cambia por sí sola ese destino.
|
|
- Añadir filas, artículos o bloques los inserta al final del capítulo activo; si no hay activo, al final del último capítulo. El selector de catálogo o bloques conserva el destino al abrirse, también si permite varias inserciones.
|
|
- Insertar encima o debajo de un detalle mantiene su capítulo, incluso si se trata de la primera o última línea. El pie no es un destino de inserción independiente.
|
|
- Eliminar el capítulo activo deja activo el capítulo anterior; si no existe, el siguiente. Al quitar todos los capítulos deja de haber capítulo activo.
|
|
|
|
### Selección, movimiento y acciones en grupo
|
|
|
|
- La cabecera selecciona el capítulo como unidad completa. El pie no se selecciona, edita, duplica, elimina ni arrastra por separado.
|
|
- Arrastrar un capítulo seleccionado entre varios capítulos seleccionados mueve esas unidades completas, manteniendo su orden relativo y el de sus detalles. El destino es un límite entre capítulos, nunca el interior de otro.
|
|
- Arrastrar detalles seleccionados los inserta consecutivamente en el destino y conserva su orden visual relativo. Si cambia el capítulo, cambia también su pertenencia. Se puede dejar vacío el capítulo de origen.
|
|
- Los destinos de detalle son posiciones dentro de un capítulo, incluido un capítulo vacío; no se permite soltar detalles antes de la primera cabecera ni fuera del último capítulo.
|
|
- Si la selección mezcla capítulos con detalles de otros capítulos, se deshabilita su arrastre conjunto y se explica el motivo. Los detalles pertenecientes a capítulos seleccionados ya están incluidos en esas unidades y no cuentan como selección mixta.
|
|
- Duplicar una selección coloca las copias de capítulos después del último capítulo seleccionado y las copias de detalles después del último detalle seleccionado de su respectivo capítulo. Se conserva el orden relativo. Los hijos de un capítulo seleccionado se procesan una sola vez.
|
|
- Duplicar capítulos copia títulos y contenido; la numeración se recalcula y las identidades son nuevas. Duplicar detalles mantiene su capítulo y los valores actuales del formulario, incluidos cambios aún no guardados.
|
|
- Borrar una selección con contenido requiere confirmación e indica los capítulos y detalles afectados, sin contar hijos dos veces. Borrar solo detalles conserva los capítulos, aunque queden vacíos.
|
|
- Las mismas restricciones de pertenencia y movimiento se aplican con ratón, teclado y pantalla táctil.
|
|
|
|
### Borrado y vuelta al modo libre
|
|
|
|
- Eliminar un capítulo vacío no requiere un diálogo adicional. Si era el último, la cotización vuelve al modo libre.
|
|
- Para eliminar un capítulo con contenido, el diálogo permite cancelar, eliminar capítulo y contenido o conservar el contenido trasladándolo a un capítulo no seleccionado para borrar. El traslado añade las líneas al final del destino, conservando el orden visual de origen.
|
|
- Si no queda otro capítulo de destino, la opción de conservación será «Quitar capítulos y conservar detalles»: elimina la agrupación completa y mantiene todas las líneas en su orden visual. Así se puede quitar el último capítulo sin perder contenido.
|
|
- La acción «Quitar todos los capítulos» está disponible siempre que haya capítulos y pide confirmación. Conserva todos los detalles y sus valores, elimina todas las pertenencias y vuelve al modo libre. Cancelar no produce cambios.
|
|
- Las operaciones afectan al formulario y marcan cambios sin guardar; solo el guardado las persiste. Cancelar un diálogo no modifica datos. Se mantiene el aviso existente al abandonar con cambios pendientes.
|
|
|
|
### Criterios de cierre de la fase
|
|
|
|
La fase 1 entrega reglas y escenarios para implementación, sin incorporar todavía código de dominio, interfaz ni persistencia. Los siguientes resultados quedan fijados:
|
|
|
|
| Caso | Acción | Resultado exigido |
|
|
| --- | --- | --- |
|
|
| F1-01 | Añadir el primer capítulo a tres detalles libres | Los tres pertenecen al nuevo capítulo en el mismo orden; no cambia el total |
|
|
| F1-02 | Cancelar el primer capítulo o introducir solo espacios | Cancelar conserva el modo libre; el título inválido impide completar el alta |
|
|
| F1-03 | Añadir un segundo capítulo y un artículo | El segundo se crea vacío y activo; el artículo se incorpora a él |
|
|
| F1-04 | Mover un capítulo con artículos y textos | Se mueve la unidad completa; la numeración se actualiza y sus detalles mantienen el orden |
|
|
| F1-05 | Mover el último detalle de un capítulo a otro vacío | El origen queda vacío con subtotal cero; el destino recalcula su subtotal; el total general no cambia |
|
|
| F1-06 | Borrar el último capítulo conservando contenido | Desaparece la agrupación y quedan todas las líneas libres con sus valores y orden |
|
|
| F1-07 | Duplicar o borrar un capítulo y alguno de sus hijos seleccionados | El hijo se procesa una sola vez; nunca queda una referencia huérfana |
|
|
| F1-08 | Ocultar importes individuales y guardar | Editor con precios visibles; PDF con cantidades, descripciones y subtotales de capítulo, sin precios ni descuentos individuales |
|
|
| F1-09 | Quitar capítulos, guardar y volver a agrupar | La preferencia PDF se conserva, se ignora en modo libre y vuelve a aplicarse en modo agrupado |
|
|
| F1-10 | Editar una cotización antigua sin añadir capítulos | Se mantiene el flujo libre y la plantilla de exportación sin capítulos |
|
|
|
|
Estos escenarios serán verificados mediante pruebas en las fases de implementación; su definición no implica que la aplicación ya los soporte.
|
|
|
|
## Auditoría inicial del código
|
|
|
|
Las rutas citadas son relativas a la raíz del repositorio. Esta tabla describe el estado previo a las fases de implementación; sus entregas se documentan al final.
|
|
|
|
| Área | Código actual | Consecuencia para los capítulos |
|
|
| --- | --- | --- |
|
|
| Especificación inicial | `doc/readme.adoc` exige conceptos libres y orden de los detalles; no define capítulos | La agrupación constituye una ampliación del modelo actual |
|
|
| Dominio | `server/src/contexts/sales/domain/entities/Quotes/Quote.ts`, `QuoteItem.ts` | `Quote` contiene una colección plana; `QuoteItem` contiene artículo, descripción, cantidad, precio y descuento. No hay pertenencia ni entidad capítulo |
|
|
| Cálculos | `QuoteItem.subtotalPrice/totalPrice`, `Quote._calculateTotalPriceItems`; `client/src/lib/calc.ts` | El subtotal de cotización suma los totales de línea después del descuento de línea; luego aplica descuento general e impuesto |
|
|
| DTOs | `shared/lib/contexts/sales/application/dto/Quote/` | Crear, obtener, actualizar y duplicar tienen contratos propios que habrá que extender de forma coherente |
|
|
| Validación | DTOs con Joi y `Quote.create` | Joi admite campos desconocidos; `Quote.create` tiene pendientes las reglas de negocio. Añadir campos al formulario no garantiza la integridad en servidor |
|
|
| Casos de uso | `server/src/contexts/sales/application/Quote/{CreateQuote,UpdateQuote,DuplicateQuote}.useCase.ts` | Crear y actualizar reconstruyen las líneas; duplicar crea nuevas entidades de detalle. Hay que transportar capítulos y reasignar sus referencias al duplicar |
|
|
| Persistencia | `server/src/contexts/sales/infrastructure/sequelize/{quote,quoteItem}.model.ts` | `quote_items` tiene clave compuesta `quote_id/item_id` y orden mediante `position`; no tiene capítulo |
|
|
| Repositorio y mapeo | `Quote.repository.ts`, `mappers/{quote,quoteItem}.mapper.ts` bajo la misma infraestructura | Obtener ordena por `position`; actualizar elimina y reinserta los detalles en una transacción. Los capítulos necesitan formar parte de esa misma escritura atómica |
|
|
| Presentación API | Presentadores de `createQuote`, `getQuote`, `updateQuote`, `duplicateQuote` | Cada uno mapea sus propias líneas. Es necesario cubrir todos; no basta con modificar GET |
|
|
| Alta SPA | `client/src/app/quotes/create.tsx` | Recoge datos generales y redirige al editor; no requiere introducir capítulos en el alta |
|
|
| Editor | `client/src/app/quotes/edit.tsx`, `components/editors/QuoteDetailsCardEditor.tsx` | React Hook Form usa `items` como lista plana mediante `useFieldArray`; recalcula importes al cambiar campos o estructura |
|
|
| Tabla y acciones | `QuoteItemsSortableDataTable.tsx`, `QuoteItemsSortableDataTableToolbar.tsx`, `QuoteItemsInlineRowActions.tsx` | TanStack Table y dnd-kit mueven filas e incluyen selección múltiple, inserción, duplicado y borrado. Todas esas operaciones necesitan respetar pertenencia |
|
|
| Artículos y textos | Selectores de catálogo y bloques en `QuoteDetailsCardEditor.tsx` | Actualmente añaden al final de la lista; en modo capítulos deben tener un capítulo de destino |
|
|
| Proveedor de datos | `client/src/app/quotes/hooks/useQuotes.tsx` | Las operaciones actuales pueden seguir usando `useDataSource` y las claves existentes; no hacen falta llamadas HTTP desde componentes |
|
|
| PDF | `ReportQuote.controller.ts`, `reporter/ReportQuote.reporter.ts`, `reporter/templates/quote/template.hbs` | La ruta `GET /quotes/:quoteId/report` carga la cotización guardada; Handlebars recorre `items` y Puppeteer genera el PDF |
|
|
| Preferencias de informe | `show_line_discount_column` atraviesa dominio, DTOs, modelo, editor y plantilla | Existe un patrón cercano para persistir la nueva preferencia de visibilidad de importes |
|
|
| Traducciones | `GetQuote.presenter.ts`, `resolveLocalizedQuoteDescriptions.ts` y reporter | Las descripciones de catálogo tienen proyección por idioma; no debe alterarse esa lógica al agrupar ni traducirse el título libre como si fuera un artículo |
|
|
|
|
Las rutas de cotizaciones ya usan `checkUser`, `getDealerMiddleware` y `handleRequest`. Se extenderán los contratos existentes manteniendo estos controles, sin crear endpoints independientes para editar capítulos.
|
|
|
|
## Modelo propuesto
|
|
|
|
Mantener `items` como lista de detalles y añadir una colección `chapters` al agregado `Quote`. Cada capítulo tendrá identidad propia y descripción. Cada detalle incorporará `chapter_id`, nullable en el modo sin capítulos. La pertenencia será explícita, no inferida por encontrar una cabecera anterior.
|
|
|
|
Contrato orientativo, adicional a los campos actuales:
|
|
|
|
```ts
|
|
chapters: Array<{ id: string; description: string }>;
|
|
items: Array<ExistingQuoteItem & { chapter_id: string | null }>;
|
|
show_chapter_line_amounts: boolean;
|
|
```
|
|
|
|
- El orden de `chapters` define el orden exterior. El orden de `items` define el orden de los detalles dentro de cada capítulo; serializar la lista plana en orden de capítulos y después de detalles.
|
|
- El servidor devuelve además el subtotal calculado por capítulo en el DTO de respuesta. El request no acepta ese subtotal como fuente de verdad.
|
|
- No guardar el pie como `QuoteItem`: produciría una línea monetaria adicional y permitiría duplicar, mover o editar un subtotal como un concepto normal.
|
|
- No añadir un booleano `has_chapters`: se deriva de la colección y evita estados contradictorios.
|
|
- El identificador del capítulo debe ser estable entre cargas y guardados. Diferenciarlo de los identificadores internos que genera `useFieldArray`; no usar su `id` interno como identidad persistente.
|
|
- No es necesario cambiar la identidad pública de los detalles para esta funcionalidad; hoy los DTOs no exponen `item_id` y actualizar reconstruye las entidades.
|
|
|
|
### Persistencia propuesta
|
|
|
|
- Nueva tabla `quote_chapters`: `quote_id`, `chapter_id`, `position`, `description`. Clave compuesta por cotización y capítulo, con borrado en cascada desde la cotización.
|
|
- `quote_items.chapter_id` nullable e indexado junto con `quote_id`. La referencia debe apuntar al capítulo de la misma cotización; considerar FK compuesta además de la validación de dominio.
|
|
- `quotes.show_chapter_line_amounts`: booleano con valor por defecto `true`.
|
|
- Persistir posiciones a partir de los arrays, como ya hace el mapper de detalles; no pedirlas también en el request.
|
|
- Cargar capítulos y detalles con orden explícito por posición. Mantener la consulta de listado sin cargar asociaciones innecesarias.
|
|
- Crear y actualizar cabecera, capítulos y detalles en la misma transacción. Con el reemplazo actual, eliminar primero detalles, después capítulos, insertar capítulos y finalmente detalles; no ejecutar en paralelo operaciones dependientes de las FK.
|
|
- Los registros históricos conservan `chapter_id = null` y ningún capítulo. No crear capítulos automáticamente al leerlos.
|
|
|
|
La preparación y aplicación del cambio de esquema será una tarea de implementación/despliegue expresamente autorizada. Esta auditoría no ejecuta migraciones ni arranca el backend. El arranque actual usa `sync({ alter: true })`: antes de desplegar hay que revisar su efecto sobre la nueva tabla, columnas, asociaciones y FK en una base aislada. No dar por validado el esquema solo porque TypeScript compile.
|
|
|
|
## Reglas de dominio y compatibilidad
|
|
|
|
1. Con `chapters.length === 0`, todos los detalles tienen `chapter_id = null`.
|
|
2. Con uno o más capítulos, cada detalle tiene exactamente un `chapter_id` existente en esa misma cotización. Rechazar referencias desconocidas, capítulos duplicados y pertenencias a otra cotización.
|
|
3. No hay capítulos anidados en esta primera versión. La descripción es el único dato editable de la cabecera; la numeración, si se muestra, se deriva de la posición.
|
|
4. El subtotal de capítulo suma el `totalPrice` de sus detalles, con descuento de línea aplicado, antes del descuento general y los impuestos. Usar `MoneyValue` y las mismas escalas y reglas de redondeo que en los cálculos actuales.
|
|
5. Los textos sin importe no contribuyen al subtotal. Los títulos y pies nunca contribuyen al total de cotización.
|
|
6. Agrupar, desagrupar, cambiar el orden o alternar la visibilidad del informe no cambia el total general.
|
|
7. Validar estructura en DTO/Joi y pertenencia en el agregado antes de persistir. Devolver errores con el patrón actual, sin corregir silenciosamente una estructura inválida enviada por API.
|
|
8. En creación y lectura antigua, campos nuevos ausentes equivalen a `chapters: []`, `chapter_id: null` y visibilidad `true`.
|
|
9. Para PUT antiguos sobre cotizaciones sin capítulos, aceptar que falten los nuevos campos. Para una cotización ya agrupada, rechazar un PUT que omita la estructura de capítulos: el cliente antiguo no conoce la agrupación y podría perderla. La eliminación de agrupación debe enviar explícitamente `chapters: []` y todos los detalles sin pertenencia. La preferencia de informe omitida en actualización conserva el valor guardado.
|
|
10. Duplicar una cotización o un capítulo crea nuevos identificadores de capítulo y remapea los detalles; conserva orden, descripciones y preferencia PDF.
|
|
|
|
La compatibilidad se refiere a conservar datos y flujos antiguos; no a permitir que un cliente antiguo elimine accidentalmente capítulos que no puede representar.
|
|
|
|
## Comportamiento del editor
|
|
|
|
Estas decisiones quedan fijadas en la fase 1 y se complementan con las reglas de selección, destinos y borrado anteriores:
|
|
|
|
| Operación | Comportamiento definido |
|
|
| --- | --- |
|
|
| Añadir el primer capítulo | Solicitar descripción y agrupar en él todos los detalles existentes, conservando su orden. En una cotización vacía, crear un capítulo vacío |
|
|
| Añadir otro capítulo | Crear un capítulo vacío después del activo, o al final si no hay capítulo activo; llevar el foco a su descripción |
|
|
| Añadir fila, artículo o bloque | Insertar en el capítulo activo; si no hay activo, usar el último. En modo sin capítulos mantener el comportamiento actual |
|
|
| Insertar encima o debajo de un detalle | Mantener el capítulo del detalle de referencia, también en sus extremos |
|
|
| Mover capítulo | Moverlo como unidad con todos sus detalles. Solo aceptar destinos entre capítulos; conservar el orden interior |
|
|
| Mover detalles | Reordenar dentro de su capítulo o trasladar a otro, actualizando pertenencia y orden en una operación. Incluir destino en capítulos vacíos; no permitir soltarlos fuera de capítulo |
|
|
| Seleccionar capítulo | Seleccionar su unidad completa para acciones de grupo. Si también se seleccionaron hijos, procesarlos una sola vez |
|
|
| Selección múltiple de detalles | Mantener la operación actual; en un traslado entre capítulos, todos los seleccionados pasan al destino conservando orden relativo |
|
|
| Selección mixta de capítulos y detalles sueltos | Deshabilitar el arrastre mixto inicialmente con explicación traducida; permite evitar movimientos ambiguos sin quitar la selección múltiple de detalles |
|
|
| Duplicar detalle | Duplicarlo en el mismo capítulo usando los valores actuales del formulario |
|
|
| Duplicar capítulo | Duplicar cabecera y contenido completo con nueva identidad y pertenencias remapeadas |
|
|
| Eliminar capítulo vacío | Eliminarlo; si era el último, volver al modo sin capítulos |
|
|
| Eliminar capítulo con contenido | Confirmar eliminación de contenido o trasladarlo a otro capítulo; sin destino disponible, permitir quitar la agrupación conservando detalles. Nunca dejar detalles huérfanos |
|
|
| Quitar todos los capítulos | Acción explícita que conserva los detalles y su orden visual, elimina sus referencias y vuelve al modo libre; también disponible con un único capítulo |
|
|
|
|
La tabla mostrará una cabecera diferenciada, detalles y pie por capítulo, siguiendo la referencia visual. Solo la descripción de la cabecera tendrá control de edición; el pie será texto monetario calculado y no tendrá selección, arrastre ni menú de acciones.
|
|
|
|
Reutilizar las celdas actuales para detalles, los diálogos y botones existentes y dnd-kit. Generar una proyección visual de cabeceras/detalles/pies a partir de los datos; esa proyección no se envía como `items` ni se incorpora al cálculo general. Mantener correspondencia explícita entre índices visuales y los índices de `items` del formulario.
|
|
|
|
Centralizar las operaciones de agrupación en el feature de cotizaciones, reutilizando el formulario y los hooks actuales. Los subtotales deben reaccionar a cantidades, precios, descuentos, altas, bajas y traslados; leer valores observados del formulario, no solo `fields`, que no refleja por sí solo cada edición de celda. El guardado enviará capítulos y pertenencias junto al resto de la cotización. Mantener aviso de cambios sin guardar.
|
|
|
|
## Informe PDF
|
|
|
|
Decisión de fase 1: la visibilidad de importes es una preferencia global por cotización, no por capítulo ni por descarga. Se guardará como `show_chapter_line_amounts`, siguiendo el patrón de `show_line_discount_column`. Un control en el editor permitirá «Mostrar importes de los detalles en el PDF» cuando haya capítulos. Valor inicial `true`, independiente de la visibilidad de descuentos y sin ocultar los importes en el editor. Quitar capítulos conserva esta preferencia para una agrupación posterior; duplicar la cotización también la conserva.
|
|
|
|
- Sin capítulos: conservar el informe actual e ignorar esta preferencia.
|
|
- Con capítulos y desglose: mostrar cabecera, detalles con sus importes y pie con subtotal.
|
|
- Con capítulos sin desglose: mostrar cantidades y descripciones de detalle, ocultando precio unitario, subtotal, porcentaje de descuento e importe total de cada detalle; mostrar el subtotal del capítulo. Evitar columnas monetarias vacías y ajustar `colspan` y cabeceras. El descuento general, los impuestos y el total de la cotización siguen en el resumen final.
|
|
- El resumen de descuentos de línea existente se mantiene como dato global según `show_line_discount_column`; nunca debe revelar precios ni porcentajes de detalles individuales cuando se oculte el desglose.
|
|
- Usar el mismo cálculo de capítulo en API y reporter, sin sumar cadenas formateadas ni confiar en subtotales enviados desde el navegador.
|
|
- Adaptar `ReportQuote.reporter.ts` y la plantilla única `template.hbs` para ambas estructuras. No duplicar plantillas completas.
|
|
- Añadir etiquetas de capítulo y subtotal a los idiomas documentales reales (`es`, `en`, `fr`). Los títulos libres se imprimen como texto escapado; conservar la resolución localizada de artículos existente.
|
|
- La preferencia persistida llega tanto a previsualización como a descarga mediante la ruta actual; no es necesario añadir parámetros de URL. La previsualización actual usa datos guardados, por lo que el cambio requiere guardar para reflejarse en el informe.
|
|
- Verificar PDF de varias páginas: cabecera junto al primer detalle cuando quepa, pie junto al último, cabeceras de tabla repetidas y capítulos largos capaces de partirse. No aplicar `break-inside: avoid` al capítulo completo.
|
|
|
|
Las cantidades y descripciones permanecen visibles en ambos modos del PDF. Con desglose, la columna de descuentos depende de `show_line_discount_column`; sin desglose, no se muestran descuentos individuales aunque esa preferencia esté activada. La cabecera del capítulo muestra número y descripción y su pie muestra el subtotal, tanto con desglose como sin él.
|
|
|
|
## Secuencia de implementación
|
|
|
|
1. **Reglas funcionales — completada.** Modos libre/agrupado, títulos, subtotales, destinos, selección, movimientos, duplicado, borrado y preferencia PDF definidos en este documento, con escenarios F1-01 a F1-10.
|
|
2. **Dominio y contratos compartidos — completada.** `QuoteChapter`, pertenencia y preferencia incorporados al dominio; DTOs y validadores ampliados. Presentadores de las cuatro respuestas adaptados al contrato. Pruebas de invariantes, subtotales, compatibilidad antigua y respuestas API.
|
|
3. **Backend y persistencia — implementación completada.** Modelos, asociaciones, mappers, repositorio transaccional y casos de uso adaptados. Lectura/escritura, rechazo de estructura inválida y duplicado con remapeo probados con dobles. Cambio de esquema preparado en `doc/capitulos-esquema.md`, sin aplicación a una base existente ni certificación de integración MariaDB.
|
|
4. **Editor — implementación completada.** Botón de alta, cabecera editable, pie calculado, destinos de inserción y movimiento de capítulos/detalles. Selección, duplicado, borrado, traslado y vuelta al modo libre cubiertos mediante pruebas del formulario, con sensores de arrastre simulados. Pendiente la inspección en navegador y la integración con una API/base autorizadas.
|
|
5. **PDF — completada.** Preferencia guardada, proyección del reporter, plantilla de ambos modos y traducciones. Pruebas HTML de contenido y ausencia de importes individuales, más generación e inspección de PDF multipágina con recursos locales. La validación integrada con datos guardados en MariaDB sigue en fase 6.
|
|
6. **Regresión y entrega.** Tests acotados, lint y compilación de paquetes afectados. Con entorno aislado autorizado, verificar esquema y ampliar E2E para crear, guardar, recargar, reordenar, duplicar y exportar ambos modos. Desplegar esquema/backend compatibles antes del cliente y evitar revertir a un backend que ignore capítulos después de empezar a guardarlos.
|
|
|
|
Cada etapa tendrá pruebas de su comportamiento; la funcionalidad no se considera completa hasta cubrir editor, API, persistencia y exportación conjuntamente.
|
|
|
|
## Casos de aceptación prioritarios
|
|
|
|
- Cotización antigua: abrir, editar, guardar, duplicar y exportar conserva líneas, orden y totales, sin capítulos creados automáticamente.
|
|
- Cotización nueva: comienza sin capítulos y permite seguir trabajando de ese modo.
|
|
- Crear primer capítulo con detalles existentes no pierde ninguna línea ni deja conceptos fuera.
|
|
- Dos capítulos con artículos y bloques de texto: guardado y recarga conservan agrupación, descripciones y orden.
|
|
- Editar cantidad, precio o descuento actualiza pie y resumen; mover detalles recalcula ambos capítulos sin cambiar el total general.
|
|
- Ejemplo: líneas con total 90 € y 50 € dan capítulo de 140 €; otra línea de 60 € da capítulo de 60 €. Subtotal general de 200 €; descuento general del 10 % e impuesto del 21 % producen 217,80 €. Reordenar u ocultar precios mantiene estos resultados.
|
|
- Capítulo vacío: pie de 0 en la moneda de la cotización; capítulo solo con textos: subtotal de 0, sin inventar importes para los textos.
|
|
- Arrastre por ratón, táctil y teclado mueve el capítulo completo; arrastre de detalles entre capítulos y hacia capítulo vacío mantiene pertenencia válida.
|
|
- Duplicado y borrado individual o múltiple no procesan hijos dos veces, no reutilizan identidades de capítulo y no dejan huérfanos.
|
|
- La API rechaza pertenencia desconocida, de otra cotización, capítulos duplicados y combinación de detalles libres con capítulos; una actualización inválida no cambia datos persistidos.
|
|
- Un fallo al escribir capítulos o detalles revierte toda la transacción.
|
|
- PDF sin capítulos y PDF con capítulos en ambos modos; combinar con descuentos visibles/ocultos, moneda adicional e idiomas documentales. Probar líneas con precios distintivos para verificar su ausencia real en el HTML sin desglose.
|
|
- PDF largo: ningún contenido desaparece; pies y encabezados quedan legibles tras los saltos de página.
|
|
|
|
## Validación realizada y hallazgos previos
|
|
|
|
### Entrega de fase 5
|
|
|
|
- `ReportQuote.reporter.ts`: proyección ordenada de cabeceras, detalles y pies. Los subtotales proceden de `Quote.getChapterSubtotal`, después del descuento de línea y antes del descuento general e impuestos. Sin capítulos se ignora `show_chapter_line_amounts`. Con capítulos y preferencia desactivada, los importes individuales tampoco se incorporan a la proyección del informe.
|
|
- `template.hbs`: una sola tabla para ambos modos. Cabeceras numeradas, pies con subtotal y texto escapado; modo sin desglose con únicamente cantidad y descripción, sin columnas económicas vacías. El resumen conserva descuentos globales, impuestos y moneda adicional. Se mantienen las traducciones de artículos y sus descripciones de respaldo.
|
|
- Etiqueta de subtotal de capítulo en los idiomas documentales `es`, `en` y `fr`, incluida en la validación de etiquetas requeridas. No se amplía el selector de idiomas documentales.
|
|
- Impresión: cabecera de tabla repetida, títulos unidos al primer detalle y pies al último cuando caben; capítulos largos y bloques de texto pueden continuar en otras páginas. Colores de capítulo preservados al imprimir, importes sin partir y resumen económico sin división entre páginas cuando cabe.
|
|
- Correcciones necesarias en el reporter: eliminar la espera de navegación tras `setContent` y cerrar Chromium en `finally`; excluir líneas de texto del acumulador de descuentos para evitar mezclar monedas nulas; imprimir el porcentaje de descuento general como cadena formateada y mostrar la moneda base real en la leyenda de cambio, que antes decía siempre `1 EUR`.
|
|
- Pruebas automatizadas: compatibilidad sin capítulos, visibilidad de importes y descuentos, orden y subtotales, capítulos vacíos y de texto, EUR/USD y moneda adicional, idiomas, escape de títulos/descripciones, traducción de artículos y cierre del navegador tanto en éxito como en fallo.
|
|
- Validación: `cd server && yarn test --runInBand src/contexts/sales` (10 suites, 72 pruebas), `yarn tsc --noEmit --pretty false`, ESLint acotado al reporter y sus pruebas y `git diff --check`. ESLint usa rutas de parser corregidas por argumento para producción y `project: null` para tests, excluidos por el tsconfig heredado; los tests sí comprueban tipos mediante ts-jest. No se cambia la configuración de lint en esta fase.
|
|
- PDF reales generados mediante `ReportQuotePresenter.toPDF`: sin capítulos (1 página), con desglose (1), sin desglose (1), capítulo de 90 líneas en ambos modos (9 páginas por modo), doce capítulos en límites de página (7) y bloque de texto de 150 líneas (5). Comprobación de texto con `pdftotext`: todas las líneas una sola vez, cabeceras repetidas en páginas con detalles, doce títulos junto al primer detalle y doce pies junto al último. Inspección visual de la muestra con desglose y de la última página con varios capítulos.
|
|
- Muestras temporales: `/tmp/uecko-chapter-pdf/`, generadas sin iniciar la API ni conectar a MariaDB. Para una verificación reproducible sin red se sustituyeron las peticiones al CSS externo por el CSS Tailwind ya compilado en `dist/client` y la imagen del pie por su copia local; no se modificó `dist`. La plantilla de producción conserva su dependencia previa del CDN de Tailwind 2 y del pie remoto; estas muestras no certifican la disponibilidad de esos recursos ni equivalencia visual exacta con ese CDN.
|
|
- No se ejecutan E2E contra la aplicación, operaciones de base de datos/Docker, instalaciones ni builds de distribución. El guardado real y la integración completa se reservan para fase 6 tras aplicar y comprobar el esquema en un entorno autorizado. Persiste una incoherencia previa ajena a capítulos: el bloque de condiciones usa `quote.default_legal_terms`, aunque el reporter expone `dealer.default_legal_terms`; no se modifica en esta fase.
|
|
|
|
### Entrega de fase 4
|
|
|
|
- Nuevo `client/src/app/quotes/chapterEditor.ts`: operaciones sobre capítulos y detalles, orden, duplicado sin doble procesamiento de hijos, traslado, borrado, desagrupación y cálculo de pies con `MoneyValue`. El guardado elimina de los capítulos sus subtotales de respuesta y de las líneas su descripción localizada de solo lectura.
|
|
- Nuevo hook `useChapterEditor.ts`: lectura de valores actuales de React Hook Form, capítulo activo, destino capturado al abrir selectores, aplicación de cambios con aviso de formulario modificado y foco del título al crear un capítulo. Los identificadores persistidos del capítulo no se confunden con los internos de `useFieldArray`.
|
|
- Nuevos `QuoteChapterCreateDialog.tsx` y `QuoteChaptersDataTable.tsx`: alta con título obligatorio, cabeceras numeradas, detalles con las celdas existentes, pies no editables, arrastre de unidades completas y detalles hacia capítulos vacíos o con contenido, selección, duplicado y diálogos de borrado/traslado. La selección mixta deshabilita el arrastre conjunto y evita procesar dos veces los hijos de un capítulo seleccionado.
|
|
- `QuoteDetailsCardEditor.tsx` integra ambos modos y conserva la tabla anterior para cotizaciones sin capítulos. El botón «Añadir capítulo» aparece en su barra de herramientas. Filas nuevas, artículos y bloques respetan el capítulo activo o el destino capturado al abrir el selector.
|
|
- La barra de herramientas, acciones de fila, filas arrastrables y `useDetailColumns.tsx` reutilizan los componentes existentes. Se ha corregido el evento de selección de Radix Checkbox; las columnas conservan sus celdas durante la escritura y se actualizan cuando cambia la moneda o el idioma.
|
|
- `edit.tsx` conserva defaults compatibles, serializa la estructura y guarda la preferencia PDF. El watcher se suscribe desde el inicio para recalcular también la primera modificación; subtotal y descuento se comparan con sus campos correspondientes. Al cargar se recupera la moneda/idioma reales y al cambiar la moneda se mantienen los valores numéricos de las líneas bajo la nueva moneda, sin añadir conversión de divisas.
|
|
- `client/src/lib/calc.ts` conserva la moneda de las líneas y de la cotización y excluye importes nulos, manteniendo el criterio del backend para líneas incompletas y textos. Los pies no entran en el cálculo general.
|
|
- Nuevos textos en `es`, `en`, `ca` y `fr`: títulos, acciones, confirmaciones, preferencia PDF y mensajes de accesibilidad del arrastre.
|
|
- La preferencia «Mostrar importes de los detalles en el PDF» queda guardada, pero su efecto en la plantilla pertenece a la fase 5. No se ha modificado todavía el reporter ni la plantilla.
|
|
|
|
Validaciones de fase 4:
|
|
|
|
- Seis suites del editor y operaciones: 27 pruebas correctas, incluidas 12 pruebas de integración React Hook Form con componentes reales de capítulos. Cubren creación/cancelación, título obligatorio, subtotales en vivo y mantenimiento del foco, movimiento de bloques, traslado a capítulo vacío, duplicado de selección, inserción en extremos, borrado/traslado, regreso al modo libre, preferencia y payload compatible con el validador compartido. El arrastre, sus destinos de teclado y sus colisiones se simulan; no son E2E en navegador.
|
|
- Traducciones: seis comprobaciones correctas de claves e interpolaciones, incluidas las de capítulos en catalán. La suite completa de recursos se ejecutó y conserva un fallo previo de bloques franceses: falta `CAP_SEP_AMBIENTES` y aparecen `CAP_INSTALACION`/`CAP_SUMINISTRO` respecto a español e inglés. Esos ficheros de bloques no se modifican en esta fase. La ejecución acotada de claves/textos excluye las dos comprobaciones de bloques.
|
|
- `cd client && yarn tsc --noEmit --pretty false` correcto. ESLint acotado sin errores; permanecen advertencias del código anterior por `any` y contexto exportado junto a componentes. La configuración tenía una severidad inválida `"none"`: se cambia por `"off"` en `client/.eslintrc.cjs` para poder ejecutar ESLint.
|
|
- Se sustituyó el import de `FormLabel` desde el barrel global en la barra por `Label` de UI, con su asociación al switch. Esto evita cargar indirectamente `react-pdf` en su prueba y resuelve el fallo previo de Jest con `pdfjs-dist` en esa suite.
|
|
- No se instalaron dependencias: los tests de formulario usan React DOM, porque falta `@testing-library/dom` en el entorno. No se ejecutaron E2E ni inspección visual: el navegador Chromium de Playwright no está instalado. No se arrancó el backend ni se aplicó el esquema; el guardado/recarga se comprueba mediante payload, validador y reset del formulario, junto a las pruebas de persistencia de fase 3.
|
|
- No se ejecutó el build de distribución para evitar recrear `dist/client`; se comprobó TypeScript sin emitir. Tampoco se ejecutó lint global con `--max-warnings 0`, dado el historial de advertencias fuera de estos cambios.
|
|
|
|
### Entrega de fase 3
|
|
|
|
- Modelos `quote.model.ts`, `quoteItem.model.ts` y nuevo `quoteChapter.model.ts`: capítulos asociados a cotización, posiciones, pertenencia nullable e índice de detalles, preferencia PDF con default `true`. Barrel de modelos actualizado.
|
|
- Nuevo `quoteChapter.mapper.ts` y ampliación de mappers de cotización y detalle: lectura de capítulos ordenados, validación del propietario, pertenencias y preferencia. La escritura genera posiciones consecutivas y orden canónico por capítulos, conservando el orden interior de detalles y textos.
|
|
- `Quote.repository.ts`: creación y reemplazo usan una transacción obligatoria; guardan cabecera, eliminan detalles antes que capítulos e insertan capítulos antes que detalles. La lectura carga ambas asociaciones por separado y con orden explícito; la búsqueda de última cotización conserva también capítulos y transacción.
|
|
- Crear y actualizar reconstruyen entidades de capítulo mediante `createQuoteChapters.ts`, validan la estructura incluso al invocarse sin controlador y asignan pertenencias a las líneas. Los casos antiguos sin capítulos siguen siendo válidos.
|
|
- La actualización carga la cotización con bloqueo `FOR UPDATE` dentro de la transacción de escritura; rechaza omitir capítulos si el registro ya está agrupado. La desagrupación exige una estructura explícita sin pertenencias. Omitir la preferencia PDF mantiene el valor persistido.
|
|
- Duplicar genera nuevos capítulos y detalles, reasigna pertenencias a la cotización nueva y conserva títulos, orden, importes y preferencia PDF. Se comprueba el fallo del resultado de dominio antes de persistir la copia.
|
|
- Pruebas de fase 3: casos de uso de creación, consulta, actualización y duplicado; mapeo en ambos sentidos; reemplazo y creación transaccionales; propagación de fallos en capítulos y detalles; metadatos y asociaciones Sequelize. El rollback se verifica con un doble transaccional y la clase de transacción existente, sin conexión a MariaDB.
|
|
- Validación: `cd server && yarn test --runInBand src/contexts/sales` (9 suites, 56 pruebas); `cd shared && yarn test --runInBand --runTestsByPath lib/contexts/sales/application/dto/Quote/QuoteChapter.dto.test.ts` (27 pruebas). TypeScript del servidor con `yarn tsc --noEmit --pretty false`; ESLint acotado a los archivos ejecutables de fase 3 usando las rutas de configuración corregidas por argumento, sin cambiar la configuración del repositorio.
|
|
- No se ejecutan backend, Docker, DDL, migraciones, instalaciones, builds de distribución ni E2E. La comprobación de esquema, FK compuesta, bloqueo y rollback real permanece documentada en `doc/capitulos-esquema.md` para integración autorizada. No hay cambios nuevos en la interfaz que requieran repetir pruebas del cliente en esta fase.
|
|
- Diferencia relevante de infraestructura: Sequelize no instala la FK compuesta de pertenencia desde los modelos definidos; la validación de aplicación sí está implementada. La restricción SQL y su convivencia con `sync({ alter: true })` quedan preparadas para revisión explícita antes de aplicar el esquema.
|
|
|
|
### Entrega de fase 2
|
|
|
|
- Dominio: nueva entidad `QuoteChapter`, con identidad, cotización propietaria y descripción obligatoria; `QuoteItem.chapterId`; `Quote.chapters`, `showChapterLineAmounts` y `getChapterSubtotal`. Se rechazan duplicados, capítulos de otra cotización y pertenencias inválidas. Sin capítulos se conservan los valores por defecto antiguos.
|
|
- Contratos: `QuoteChapter.dto.ts`, DTOs de crear, consultar, actualizar y duplicar y sus barrel exports. Los requests admiten omitir los campos para compatibilidad y no introducen defaults que confundan omisión con desagrupación explícita. Las respuestas incluyen capítulos con subtotal calculado, pertenencia nullable y preferencia PDF.
|
|
- Validadores de crear/actualizar: verifican estructura, UUID v4, títulos, unicidad, pertenencia y preferencia booleana. Rechazan datos de subtotal o anidación dentro de capítulos enviados como entrada. La comprobación de PUT antiguo frente a una cotización ya agrupada requiere cargar el registro y corresponde a la fase 3.
|
|
- Los cuatro presentadores API se adaptan ahora para satisfacer los contratos de respuesta requeridos. No hay cambios de esquema, repositorio ni casos de uso: estos todavía no reconstruyen ni guardan capítulos. Esta fase no constituye una versión desplegable de la funcionalidad completa.
|
|
- Los textos sin importe se excluyen de la suma general, al igual que de la suma de capítulos. Esto evita intentar sumar importes nulos en la moneda por defecto a una cotización de otra moneda.
|
|
- Tests de contratos en shared: 27 pruebas correctas. Tests de dominio y regresión en servidor: 24 pruebas correctas; tests de los cuatro presentadores: 8 pruebas correctas.
|
|
- TypeScript de servidor y cliente: `yarn tsc --noEmit --pretty false` correcto en ambos paquetes.
|
|
- ESLint acotado a los archivos ejecutables modificados del servidor: sin errores y con tres advertencias previas por parámetros `context` sin usar en presentadores. Para ejecutarlo se usaron `--ignore-path ../.gitignore` y un `tsconfigRootDir` absoluto mediante `--parser-options`: la configuración habitual intenta leer rutas relativas inexistentes desde `server/`. No se modifica esa configuración fuera del alcance.
|
|
- El test nuevo de shared importa las funciones de `@jest/globals` explícitamente: en este entorno `shared/node_modules/@types` no existe y el `typeRoots` del paquete impide resolver los globales Jest automáticamente. No se instalaron dependencias ni se cambió la configuración global de pruebas.
|
|
- `git diff --check` correcto. No se ejecutan builds de distribución, lint global, E2E ni operaciones de infraestructura: se comprueban tipos sin emitir y pruebas acotadas; la validación de esquema y flujos completos corresponde a fases posteriores.
|
|
|
|
### Auditoría y fase 1
|
|
|
|
Validación del cierre de fase 1: revisión de coherencia entre decisiones funcionales, tabla del editor, comportamiento PDF y escenarios F1-01 a F1-10. `git diff --no-index --check /dev/null doc/capitulos-cotizaciones.md` correcto. No se repiten tests, lint, builds ni E2E porque esta fase solo modifica la especificación; los resultados siguientes corresponden a la auditoría inicial y no verifican capítulos implementados.
|
|
|
|
Estado inicial: `git status --short` limpio. Auditoría estática de especificación, rutas, dominio, DTOs, persistencia, editor, cálculos, traducciones y exportación.
|
|
|
|
Comandos ejecutados desde cada paquete:
|
|
|
|
```bash
|
|
# server
|
|
yarn test --runInBand --runTestsByPath src/contexts/sales/application/Quote/UpdateQuote.useCase.test.ts src/contexts/sales/infrastructure/mappers/quote.mapper.test.ts src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.test.ts
|
|
|
|
# client
|
|
yarn test --runInBand --runTestsByPath src/app/quotes/components/QuoteItemsSortableDataTable.test.tsx src/app/quotes/components/QuoteItemsSortableDataTableToolbar.test.tsx src/app/quotes/components/QuoteItemsInlineRowActions.test.tsx
|
|
```
|
|
|
|
- Servidor: 3 suites y 12 pruebas correctas. Son pruebas con dobles de repositorio/mappers y renderizado HTML; no validan una base real ni generan PDF.
|
|
- Cliente: 2 suites correctas, 4 pruebas correctas; la suite de la barra de herramientas falla antes de ejecutar sus pruebas porque Jest no transforma el ESM de `pdfjs-dist`, importado indirectamente desde el barrel de componentes y `react-pdf`. Fallo previo a esta propuesta; no se ha modificado su configuración.
|
|
- No se ejecutaron builds ni lint: la entrega solo incorpora documentación y no cambia código ejecutable. No se ejecutó la batería completa.
|
|
- No se ejecutaron E2E, backend, Docker ni operaciones de base de datos: requieren infraestructura fuera del alcance de esta auditoría. No se instaló ninguna dependencia.
|
|
|
|
Inconsistencias observadas, sin cambios fuera del alcance:
|
|
|
|
- `AGENTS.md` indica que no se localizaron tests de servidor ni E2E; el repositorio actual sí contiene ambos y scripts Playwright en el paquete raíz.
|
|
- La interfaz carga `es/en/ca/fr`, mientras los selectores de idioma documental y el reporter admiten `es/en/fr`. Añadir textos de interfaz en los cuatro recursos; no asumir PDF catalán implementado.
|
|
- Los DTOs de línea tienen diferencias previas: crear declara `price`, actualizar/obtener usan `subtotal_price`, y el validador de actualización menciona `article_id` mientras el contrato y caso de uso usan `id_article`. Ampliar las pruebas de contrato sin normalizar campos ajenos a esta funcionalidad.
|
|
- El watcher de `edit.tsx` compara en varios puntos el subtotal o descuento calculado con `formData.total_price` en lugar del campo correspondiente. Revisar esa ruta al incorporar el recálculo de capítulos para no añadir escrituras redundantes; no se corrige en esta auditoría.
|
|
|
|
### Ampliación: plegado visual del editor
|
|
|
|
- `QuoteChaptersDataTable.tsx`: botón accesible con `aria-expanded` y etiquetas traducidas en `es/en/ca/fr`. Estado local por capítulo, vinculado a su identificador; no forma parte del formulario ni de los DTO. Conserva el título, subtotal y acciones del capítulo visibles.
|
|
- `QuoteItemsSortableTableRow.tsx`: soporte para filas ocultas. Los campos permanecen montados para conservar valores y validación; los detalles y destinos vacíos plegados se excluyen del movimiento de filas. Los capítulos completos siguen pudiendo desplazarse.
|
|
- `QuoteChapterEditor.test.tsx`: plegar/desplegar sin alterar datos ni payload, conservar campos editados y subtotal, ocultar acciones de fila, mantener plegado al reordenar y desplegar copias de forma independiente.
|
|
- Validación: 22 pruebas de editor y operaciones de capítulos, cinco comprobaciones de traducciones, TypeScript sin emisión y ESLint acotado (dos advertencias previas de contexto/`any` en la fila reutilizable). La suite de editor emite un aviso de actualización asíncrona de paginación fuera de `act`; las dos pruebas nuevas de plegado se ejecutan también aisladas y pasan sin ese aviso. No se modifican ni repiten las pruebas de servidor/PDF, ni se ejecutan E2E o builds de distribución: el cambio se limita al estado visual del cliente.
|