47 KiB
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 |
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:
chapters: Array<{ id: string; description: string }>;
items: Array<ExistingQuoteItem & { chapter_id: string | null }>;
show_chapter_line_amounts: boolean;
- El orden de
chaptersdefine el orden exterior. El orden deitemsdefine 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 suidinterno como identidad persistente. - No es necesario cambiar la identidad pública de los detalles para esta funcionalidad; hoy los DTOs no exponen
item_idy 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_idnullable e indexado junto conquote_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 defectotrue.- 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 = nully 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
- Con
chapters.length === 0, todos los detalles tienenchapter_id = null. - Con uno o más capítulos, cada detalle tiene exactamente un
chapter_idexistente en esa misma cotización. Rechazar referencias desconocidas, capítulos duplicados y pertenencias a otra cotización. - 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.
- El subtotal de capítulo suma el
totalPricede sus detalles, con descuento de línea aplicado, antes del descuento general y los impuestos. UsarMoneyValuey las mismas escalas y reglas de redondeo que en los cálculos actuales. - Los textos sin importe no contribuyen al subtotal. Los títulos y pies nunca contribuyen al total de cotización.
- Agrupar, desagrupar, cambiar el orden o alternar la visibilidad del informe no cambia el total general.
- 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.
- En creación y lectura antigua, campos nuevos ausentes equivalen a
chapters: [],chapter_id: nully visibilidadtrue. - 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. - 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
colspany 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.tsy la plantilla únicatemplate.hbspara 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: avoidal 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
- 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.
- 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. - 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. - 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.
- 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.
- 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 deQuote.getChapterSubtotal, después del descuento de línea y antes del descuento general e impuestos. Sin capítulos se ignorashow_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,enyfr, 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
setContenty cerrar Chromium enfinally; 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 siempre1 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 ygit diff --check. ESLint usa rutas de parser corregidas por argumento para producción yproject: nullpara 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 conpdftotext: 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 endist/clienty 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 exponedealer.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 conMoneyValue. 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 deuseFieldArray. - Nuevos
QuoteChapterCreateDialog.tsxyQuoteChaptersDataTable.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.tsxintegra 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.tsxreutilizan 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.tsxconserva 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.tsconserva 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,cayfr: 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_AMBIENTESy aparecenCAP_INSTALACION/CAP_SUMINISTROrespecto 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 falsecorrecto. ESLint acotado sin errores; permanecen advertencias del código anterior poranyy contexto exportado junto a componentes. La configuración tenía una severidad inválida"none": se cambia por"off"enclient/.eslintrc.cjspara poder ejecutar ESLint.- Se sustituyó el import de
FormLabeldesde el barrel global en la barra porLabelde UI, con su asociación al switch. Esto evita cargar indirectamentereact-pdfen su prueba y resuelve el fallo previo de Jest conpdfjs-disten esa suite. - No se instalaron dependencias: los tests de formulario usan React DOM, porque falta
@testing-library/domen 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.tsy nuevoquoteChapter.model.ts: capítulos asociados a cotización, posiciones, pertenencia nullable e índice de detalles, preferencia PDF con defaulttrue. Barrel de modelos actualizado. - Nuevo
quoteChapter.mapper.tsy 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 UPDATEdentro 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 conyarn 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.mdpara 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,showChapterLineAmountsygetChapterSubtotal. 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 falsecorrecto en ambos paquetes. - ESLint acotado a los archivos ejecutables modificados del servidor: sin errores y con tres advertencias previas por parámetros
contextsin usar en presentadores. Para ejecutarlo se usaron--ignore-path ../.gitignorey untsconfigRootDirabsoluto mediante--parser-options: la configuración habitual intenta leer rutas relativas inexistentes desdeserver/. No se modifica esa configuración fuera del alcance. - El test nuevo de shared importa las funciones de
@jest/globalsexplícitamente: en este entornoshared/node_modules/@typesno existe y eltypeRootsdel paquete impide resolver los globales Jest automáticamente. No se instalaron dependencias ni se cambió la configuración global de pruebas. git diff --checkcorrecto. 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:
# 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 yreact-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.mdindica 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 admitenes/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 usansubtotal_price, y el validador de actualización mencionaarticle_idmientras el contrato y caso de uso usanid_article. Ampliar las pruebas de contrato sin normalizar campos ajenos a esta funcionalidad. - El watcher de
edit.tsxcompara en varios puntos el subtotal o descuento calculado conformData.total_priceen 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 conaria-expandedy etiquetas traducidas enes/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/
anyen la fila reutilizable). La suite de editor emite un aviso de actualización asíncrona de paginación fuera deact; 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.