- Implement drag-and-drop functionality for chapters using the folder icon as the activator, improving accessibility and user experience. - Refactor chapter deletion dialog to clearly separate options for keeping or deleting chapter contents, with appropriate translations and UI adjustments. - Update empty state handling for quotes without items, providing clear actions to add content. - Adjust translations for clarity and consistency across multiple languages. - Remove unnecessary empty columns in the chapter details table, streamlining the layout. - Enhance visual styling with borders and spacing adjustments for better chapter delineation. - Add tests to cover new functionalities and ensure existing features remain intact.
73 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
Integración MariaDB: preparación y acceso pendiente
- Nueva suite explícita
cd server && yarn test:mariadb, separada de los tests habituales. Catorce casos preparados para esquema histórico, guardado/recarga, presentador API/HTML, movimiento, duplicado, desagrupación, rollback real mediante triggers, bloqueo concurrente, segundo arranque y FK compuesta. - El fixture exige MariaDB y acceso específico por variables
UECKO_MARIADB_TEST_*. Crea y elimina únicamente un esquema nuevo aleatorio con datos ficticios, sin usar la configuración ni las tablas de la aplicación. Seis pruebas unitarias de seguridad del fixture. - No se da por completada esta fase: el acceso facilitado a
localhost:3306funciona, pero el servidor se identifica como MySQL 8.0.42. La suite rechaza ese motor antes del DDL y deja intactauecko. Falta un destino MariaDB o autorización para cambiar el motor de validación. Procedimiento, permisos y límites endoc/capitulos-esquema.md.
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.
Ampliación: agrupación visual de capítulos
QuoteChaptersDataTable.tsx: estilos mediante clases TailwindCSS inline, utilizando exclusivamente los colores existentes y griseszinc. No se añade CSS externo ni se introducen tonos azul/verde. Se mantiene la paleta de los controles actuales.- Columna estructural
chapter_treede 44 px, exclusiva del editor con capítulos. En la cabecera contiene el control de plegado; en detalles, acciones y subtotal queda vacía para mantener la alineación, sin guías ni bordes verticales. Esta columna no forma parte del formulario, DTO ni PDF. - Numeración destacada, icono de carpeta, cabecera gris y subtotal diferenciado, con separadores horizontales y sin marco lateral. Separación de 20 px entre capítulos y sangría de las descripciones. La distinción de capítulos se apoya en límites, espacio y numeración, sin colores alternos. Las cantidades e importes conservan sus tamaños y alineación.
- Ajustados todos los
colspan: cabeceras, detalles, acciones de fila, capítulos vacíos y pies. Los campos permanecen montados al plegar; no cambian selección, traslado, duplicado ni cálculos. - Dos comprobaciones nuevas verifican que las filas cubran las mismas columnas al mostrar acciones, plegar o tener capítulos vacíos, con descuentos visibles y ocultos; también verifican que no cambie el payload. No se añaden pruebas que reproduzcan el CSS.
- Validación: 24 pruebas entre editor y operaciones de capítulos; TypeScript sin emisión y ESLint del componente/tests sin errores ni advertencias. Tailwind compila las clases del componente hacia
/tmp/uecko-chapters-neutral.css, sin modificardistni instalar dependencias. Persiste el aviso previo de React/TanStack sobre una actualización de paginación fuera deacten la suite de editor. - Inspección con Chromium de muestra DOM del harness en ambos estados, verificando guía, separación, fondos neutros y subtotal visible. Los campos y sensores de la muestra son dobles de test, por lo que no es un E2E contra la API autenticada. El fixture temporal se retiró; las muestras permanecen en
/tmp/uecko-chapters-neutral-*. - No se ejecutan servidor, base de datos, pruebas PDF ni builds de distribución para este cambio visual. Se conservan los cambios previos de integración MariaDB sin modificación funcional.
Ampliación: orden de columnas del editor
- Orden con capítulos: despliegue/plegado, arrastre, selección y resto de columnas existentes. El control de despliegue se encuentra únicamente en la cabecera del capítulo; los detalles reservan esa columna vacía, sin líneas verticales.
- Orden sin capítulos: arrastre, selección y resto de columnas; no se añade la columna de despliegue.
useDetailColumns.tsxestablece arrastre antes de selección yQuoteChaptersDataTable.tsxalinea las cabeceras de capítulo con ese orden. El control de la primera columna muestraFolderIconal estar plegado yFolderOpenIconal estar desplegado; se elimina la carpeta decorativa del título. Se mantienen las clases Tailwind inline existentes.- Dos casos de prueba comprueban posición de controles en ambos modos, cabecera de selección, alineación de descripción/cantidad y ausencia de la columna de despliegue en cotizaciones libres. El doble de cabecera del formulario ahora renderiza su contenido, para verificar también la tabla sin capítulos.
- Validación: 19 pruebas de editor y asa de arrastre, TypeScript sin emisión, ESLint acotado y
git diff --checkcorrectos. Persiste el aviso previo de paginación React/TanStack fuera deact. No se repiten PDF, base de datos ni E2E, al limitarse el cambio al orden de columnas del editor.
Ajuste visual: sin líneas verticales
- Se retiran las guías de ramificación, el borde lateral izquierdo, el borde derecho y la franja vertical de fondo. Se conserva vacía la columna de despliegue en las filas de detalle para respetar el orden y alineación de controles.
- Los capítulos se distinguen por separación de bloques, numeración, fondos grises de título/subtotal y separadores horizontales. Se mantienen los iconos de carpeta abierta/cerrada.
- Las muestras visuales de
/tmp/uecko-chapters-neutral-*corresponden a la revisión anterior, con guías; no representan este ajuste. La validación de este cambio se limita a las seis pruebas existentes de plegado, orden y alineación, ESLint acotado ygit diff --check; no se repiten E2E/PDF ni se modifica persistencia por tratarse de una retirada de decoración.
Ampliación: barra de acciones con grupos de botones
QuoteItemsSortableDataTableToolbar.tsxreutilizaButtonGroupexistente (importación directa) y losButtonde la UI. Agrupa altas de fila/artículo/bloque o acciones de selección en un bloque compacto; las acciones de capítulos forman otro grupo. Botonesghostpequeños, marcos neutros y clases Tailwind inline; no se instalan componentes ni dependencias.- Unifica la estructura de la barra con y sin selección. Se conservan tooltips, textos accesibles, contador seleccionado y los callbacks de tabla o de selección de capítulos. Eliminar mantiene el color destructivo existente y sus confirmaciones siguen en los flujos actuales.
- La nueva prop opcional
viewControlscoloca el interruptor de importes PDF junto al de descuentos, fuera de los grupos de comandos.QuoteChaptersDataTable.tsxyQuoteDetailsCardEditor.tsxadaptan los botones de capítulos para mantener una presentación uniforme, también antes de crear el primer capítulo. - Disposición con salto de línea: grupos de botones y controles se adaptan al ancho. Chromium verifica las muestras de alta y selección a 1440 y 375 px sin desbordamiento de la barra, con ambos interruptores visibles. HTML generado desde el harness del editor; esta revisión no constituye un E2E con la API autenticada. Artefactos temporales en
/tmp/uecko-toolbar-*; el fixture temporal se retiró. - Dos pruebas nuevas verifican delegación de duplicar/eliminar/deseleccionar en modo líneas y capítulos, y conservación de opciones PDF/descuento fuera de los grupos de botones. Se mantienen las pruebas de confirmación al ocultar descuentos.
- Validación: 22 pruebas de barra y editor; TypeScript sin emisión; compilación Tailwind hacia
/tmp; ESLint acotado sin errores, con cuatro advertenciasanyprevias en los componentes de barra/editor;git diff --checkcorrecto. Permanece el aviso previo de paginación React/TanStack fuera deacten la suite de editor. - No se ejecutan base de datos, PDF, builds de distribución ni E2E contra la aplicación para este cambio de presentación. No se alteran los datos ni los cálculos de la cotización.
Ampliación: cabeceras por capítulo, estado vacío y numeración en carpeta
QuoteChaptersDataTable.tsxmuestra las cabeceras de columnas después del título de cada capítulo con contenido, dentro de su grupo de filas. Se ocultan al plegarlo. Uncolgroupcomún mantiene la alineación; las cotizaciones sin capítulos conservan su cabecera global.- La casilla de la cabecera selecciona o deselecciona únicamente los detalles de ese capítulo. La casilla del título sigue seleccionando el capítulo completo.
- Los capítulos vacíos muestran
Emptycon mensaje, icono y acciones para añadir fila vacía, artículo o bloque. Se añadeui/empty.tsxsiguiendo la composición de Empty de shadcn/ui, adaptada a React 18 y Tailwind 3 sin dependencias nuevas. Los textos están traducidos enes/en/ca/fr. - Las acciones usan el identificador explícito del capítulo vacío.
QuoteDetailsCardEditor.tsxyuseChapterEditor.tsconservan ese destino al abrir un selector, aunque cambie el foco antes de elegir contenido. Al añadir el primer detalle, el estado vacío se sustituye por las cabeceras y la fila. - El número de orden sale del título y aparece como
Badgegris superpuesto al icono de carpeta de la primera columna. Se mantiene al plegar y se actualiza al reordenar; no altera la descripción ni el PDF. Las etiquetas accesibles del botón siguen incluyendo el número. - Validación: 28 pruebas de editor/barra y cinco de traducciones; TypeScript sin emisión; ESLint acotado sin errores (tres advertencias
anyprevias del editor); Tailwind compilado en/tmp;git diff --check. Persisten los avisos previos de paginación fuera deacty de Browserslist desactualizado. - Revisión local con Chromium a 1440 y 375 px sobre HTML del harness: badge sobre carpeta, una cabecera para el capítulo con contenido y tres acciones en el vacío. En móvil la tabla mantiene su desplazamiento horizontal. Artefactos temporales
/tmp/uecko-chapter-empty-*; el fixture temporal se retiró. No es una prueba E2E con API autenticada. - No se ejecutan base de datos, PDF ni builds de distribución: esta ampliación afecta a la presentación e inserción de detalles del editor, sin cambios en persistencia o informes.
Ajuste: alineación del arrastre y plegado animado
QuoteChaptersDataTable.tsx: el botón de arrastre del título usa el mismo ancho de 16 px que el de las líneas. Ambos iconos comparten eje horizontal, conservando el orden de columnas y el badge de la carpeta.useChapterCollapse.ts: transición de altura y opacidad de 200 ms mediante Web Animations API. Se animan el contenido y el espaciado de las celdas antes de ocultar sus filas; incluye cabeceras, detalles, acciones y estado vacío. Los campos permanecen montados; título y subtotal siguen visibles. Se respetaprefers-reduced-motiony se pliega inmediatamente cuando no hay soporte para animación.- Durante el cierre, las filas dejan de ser interactivas mediante
inertyaria-hidden; el arrastre de detalles/destinos vacíos se desactiva desde el inicio. Al invertir el sentido, se cancela la transición anterior y se parte de la posición actual. Al desmontar, se cancelan las animaciones pendientes. - Dos pruebas nuevas del editor verifican ocultación tras finalizar, reversión sin ocultaciones obsoletas, conservación de valores y movimiento reducido. Validación: 26 pruebas del editor, TypeScript sin emisión, ESLint acotado sin errores y
git diff --check. Persiste el aviso previo de paginación React/TanStack fuera deacten la suite completa; las nuevas pruebas aisladas pasan sin ese aviso. - Chromium local verifica geometría sobre el HTML actual del harness y ejecuta el hook real: centros de ambos iconos en 117,53125 px; altura del grupo de 222 a 117 px con estados intermedios de 170 y 184,6875 px; recuperación de altura/valores originales y ausencia de animación con movimiento reducido. Artefactos en
/tmp/uecko-chapter-animation-*; fixture temporal retirado. Esta comprobación no es un E2E con la API autenticada. - No se ejecutan builds de distribución, PDF ni base de datos: son cambios de presentación del editor, sin cambios de persistencia o informes. La compilación Tailwind se dirige a
/tmpy conserva el aviso previo de Browserslist desactualizado.
Corrección: resaltado conjunto de detalle activo y acciones
QuoteItemsSortableTableRow.tsxidentifica la fila activa mediantedata-active-item.QuoteChaptersDataTable.tsxaplica el contorno a sus celdas, porque los bordes de las filas no se dibujan en la tabla conborder-separate.- Detalle y acciones comparten fondo rojo claro, borde izquierdo rojo de 4 px y contorno exterior fino; se retira el separador intermedio. Las acciones ocupan todo el ancho de la tabla y empiezan junto al borde izquierdo, como en la presentación original. El estilo de selección no se aplica a los capítulos ni a otras líneas.
- Validación: 26 pruebas existentes del editor, TypeScript sin emisión, ESLint acotado sin errores (dos advertencias previas en el componente de fila) y
git diff --check. Chromium local verifica igualdad de fondos, continuidad del contorno y ausencia de espacio o borde entre detalle y acciones. Muestra en/tmp/uecko-active-row.png, generada desde el harness del editor; fixture temporal retirado. - No se ejecutan base de datos, PDF, builds de distribución ni E2E autenticados: es una corrección de estilos. Persisten los avisos previos de paginación fuera de
acty Browserslist desactualizado.
Ajuste: selección exclusiva de líneas de detalle
- Se elimina el checkbox de los títulos de capítulo, manteniendo su celda vacía para alinear las columnas. La selección se limita a los detalles; el checkbox de cabecera de columnas conserva la selección conjunta de los detalles de ese capítulo.
- Se retira del editor el estado de selección múltiple de capítulos y su bloqueo por selección mixta. Los capítulos se mueven individualmente con todo su contenido y conservan sus botones propios de duplicar/eliminar. La barra actúa sobre los detalles seleccionados.
- Se adaptan las pruebas existentes de duplicación, selección y alineación: 26 pruebas del editor correctas, TypeScript sin emisión, ESLint acotado sin errores y
git diff --check. Persiste el aviso previo de paginación fuera deact. No se ejecutan PDF, base de datos ni E2E: el cambio se limita a la selección en el editor.
Ajuste: controles visibles al pasar el cursor
- Las filas de detalle muestran arrastre y selección al pasar el cursor por su propia fila, con y sin capítulos. El checkbox marcado permanece visible aunque el cursor salga; al desmarcarlo vuelve a ocultarse. El asa también permanece visible durante el arrastre.
- En el título de capítulo se muestran arrastre, duplicar y eliminar únicamente al pasar por ese título; el hover de los detalles no activa esos botones. Carpeta y badge siguen visibles. Se usan grupos Tailwind con nombre por tipo de fila y opacidad para mantener la alineación.
- Cada control se revela también al recibir foco de teclado. En dispositivos sin hover permanecen visibles para permitir su uso táctil. La transición de opacidad respeta movimiento reducido.
- Cambios en
QuoteChaptersDataTable.tsx,QuoteItemsSortableTableRow.tsx,QuoteItemsSortableDragHandleCell.tsxyuseDetailColumns.tsx. Validación: 27 pruebas existentes de editor/arrastre; ESLint acotado sin errores (dos advertencias previas de la fila); Tailwind compilado en/tmp;git diff --check. La última excepción de checkbox marcado se comprueba además con CSS real en Chromium. - Chromium local verifica ocultación, hover por fila, checkbox marcado/desmarcado, foco de teclado y controles táctiles, con y sin capítulos. Para comprobar selectores de escritorio se desactiva solo la regla táctil en el fixture, ya que Chromium sin interfaz informa
hover:none; la prueba táctil usa la hoja original. Artefactos/tmp/uecko-row-controls-*; fixture temporal retirado. No es un E2E autenticado. - No se ejecutan TypeScript, builds de distribución, PDF ni base de datos: el cambio solo añade clases y no altera tipos, datos o informes. Persisten los avisos previos de paginación fuera de
acty Browserslist desactualizado.
Ajuste: márgenes simétricos de carpeta y eliminar capítulo
QuoteChaptersDataTable.tsx: aumenta el espacio izquierdo de la carpeta y amplía la columna de despliegue de 44 a 56 px para alojarlo sin invadir la columna de arrastre. Conserva tamaños de iconos y botones, badge y alineación de detalles.- Chromium local mide 20 px tanto entre el borde izquierdo y la carpeta como entre la papelera y el borde derecho, en ambos capítulos a 1440 y 1024 px. El eje de las asas de título/detalle sigue coincidiendo (diferencia 0 px). HTML exportado del harness en
/tmp/uecko-chapter-spacing.html; fixture temporal retirado. - Validación acotada: cuatro casos existentes de alineación/orden de columnas, ESLint del componente, compilación Tailwind hacia
/tmpygit diff --check. No se ejecutan TypeScript, PDF, base de datos, builds de distribución ni E2E autenticados por ser un ajuste de espaciado. Persiste el aviso previo de Browserslist desactualizado.
Ajuste: título alineado con el arrastre de sus conceptos
- El título de capítulo ocupa las columnas posteriores a la carpeta y empieza en el mismo eje vertical que el icono de arrastre de sus detalles. El asa del capítulo se sitúa a la derecha del título, junto a duplicar y eliminar; conserva eventos de arrastre, foco y visibilidad al pasar el cursor.
QuoteChaptersDataTable.tsxajusta el colspan y espaciado del título.QuoteChapterEditor.test.tsxadapta las comprobaciones de estructura y badge a las dos celdas del título. Se mantienen orden de columnas de los detalles y márgenes exteriores de 20 px.- Validación: cinco casos existentes de alineación, orden y numeración (ejecutados también en el fixture temporal), ESLint sin errores, compilación Tailwind en
/tmpygit diff --check. Chromium local mide diferencia de 0 px entre inicio del texto del título y lado izquierdo del icono de concepto, a 1440 y 1024 px, manteniendo márgenes de carpeta/papelera de 20 px. Fixture temporal retirado; muestra/tmp/uecko-chapter-title.png. - No se ejecutan TypeScript, PDF, base de datos, build de distribución ni E2E autenticado: es un cambio local de disposición, sin cambios de tipos o datos. Persiste el aviso previo de Browserslist desactualizado.
Ajuste: carpeta como asa de arrastre del capítulo
QuoteChaptersDataTable.tsxvinculasetActivatorNodeRef, atributos y listeners deuseSortableal botón Folder/FolderOpen. Se elimina el asa independiente de la derecha. La carpeta conserva badge, márgenes, clic de plegado y estado accesiblearia-expanded.- El puntero muestra
grab/grabbingy una ayuda nativa (title) explica que arrastra todo el capítulo y que el clic pliega/despliega. La misma ayuda se expone conaria-descriptiony se traduce enes/en/ca/fr. - Con teclado, Espacio inicia el arrastre y las flechas eligen destino; Entrar confirma el movimiento. Fuera de un arrastre, Entrar conserva el clic nativo de plegado. El click no cambia el plegado mientras se arrastra; el sensor real suprime el clic tras completar un movimiento con puntero.
QuoteChapterEditor.test.tsxadapta las comprobaciones anteriores y verifica activador/ref, listeners de puntero, separación de Enter/Espacio, ayuda y conservación de datos al plegar. Validación: 27 pruebas de editor y cinco comprobaciones de traducciones; TypeScript sin emisión; ESLint acotado sin errores;git diff --check.- Chromium ejecuta el componente, hook y sensores reales desde un bundle temporal en
/tmp, con formulario local y sin API. Comprueba clic, reordenación mediante carpeta de capítulos abiertos/cerrados conservando contenido y plegado, y movimiento/plegado con teclado. Artefactos/tmp/uecko-folder-drag-*y/tmp/uecko-folder-drag.png; no se añaden dependencias ni fixtures al repositorio. - No se ejecutan PDF, base de datos, build de distribución ni E2E autenticado: cambia únicamente la interacción del editor. Persisten los avisos previos de paginación fuera de
acty Browserslist desactualizado.
Ajuste: eliminar columna vacía a la izquierda de los conceptos
- Se elimina la columna estructural
chapter_treey sus celdas vacías de los detalles, cabeceras, subtotales y capítulos vacíos. Las líneas empiezan directamente por arrastre y selección, igual que en una cotización sin capítulos. - El título usa una única celda que abarca todas las columnas; carpeta, descripción y acciones se disponen dentro de ella con Tailwind inline. Conserva posición de la carpeta, badge, márgenes exteriores de 20 px y sus funciones de arrastre/plegado, sin reservar espacio en las filas inferiores.
QuoteChaptersDataTable.tsxutiliza directamente las columnas de detalle y ajusta los colspan.QuoteChapterEditor.test.tsxadapta las comprobaciones de alineación, orden y badge a la nueva estructura. Validación: 27 pruebas de editor, TypeScript sin emisión, ESLint sin errores, Tailwind hacia/tmpygit diff --check.- Chromium sobre HTML del harness confirma ausencia de columnas extra, arrastre a 8 px del inicio de la fila, checkbox en segunda columna y coincidencia de columnas/span de título, cabecera, detalles, subtotal y estado vacío, a 1440 y 1024 px. Carpeta/papelera mantienen márgenes de 20 px. Muestra
/tmp/uecko-chapter-gutter.png; fixture temporal retirado. - No se ejecutan PDF, base de datos, build de distribución ni E2E autenticado por ser un cambio de disposición del editor. Persisten los avisos previos de paginación fuera de
acty Browserslist desactualizado.
Ajuste: contorno gris del capítulo completo
QuoteChaptersDataTable.tsxañade bordes laterales grises de 1 px a las celdas exteriores de título, cabeceras, conceptos, estado vacío y subtotal. Junto con los bordes superior/inferior existentes forman un único contorno por capítulo, sin líneas verticales entre columnas ni bordes en la separación entre capítulos.- Se conserva el resaltado rojo del conjunto detalle/acciones activo. El contorno sigue visible en capítulos vacíos y plegados; no se añaden columnas ni se cambian interacciones.
- Validación: cuatro casos existentes de alineación/orden y un fixture visual temporal; ESLint sin errores; Tailwind compilado en
/tmp;git diff --check. Chromium verifica bordes exteriores de 1 px grises, ausencia de bordes verticales interiores y separación sin contorno, en estado normal, activo y plegado. Muestras/tmp/uecko-chapter-border*.png; fixture temporal retirado. - No se ejecutan TypeScript, PDF, base de datos, build de distribución ni E2E autenticado: el cambio añade únicamente clases de estilo. Persiste el aviso previo de Browserslist desactualizado.
Ajuste: etiqueta breve de importes PDF
- Se sustituye la etiqueta por «Importes por línea en PDF» y se actualizan sus traducciones en
es/en/ca/fr. Activado sigue mostrando los importes de cada detalle en el PDF; desactivado mantiene únicamente los subtotales de capítulo. No se invierte ni cambia la preferencia guardada. - Se adapta la prueba de preferencia del editor. Validación acotada: prueba de persistencia PDF y cinco comprobaciones de traducciones;
git diff --check. No se ejecutan builds, TypeScript, base de datos, generación de PDF ni E2E por ser un cambio de texto.
Ajuste: diálogo de eliminación con alternativas separadas
QuoteChaptersDataTable.tsxdistingue eliminar un capítulo de eliminar únicamente conceptos seleccionados. El diálogo de capítulo presenta dos secciones con nombre accesible, explicación propia y botón independiente: «Conservar conceptos» con traslado al destino elegido y «Eliminar capítulo y conceptos» con tratamiento destructivo.- El selector de destino pertenece únicamente al bloque de conservación; el botón «Trasladar conceptos y quitar capítulo» permanece deshabilitado hasta elegir destino. «Cancelar» está separado en el pie. Si se elimina el último capítulo, la alternativa de conservación explica que los conceptos quedarán sin agrupar y conserva sus importes.
- El diálogo tiene ancho adaptable, botones con texto ajustable y desplazamiento vertical si falta altura. Se conservan la lógica de eliminación/traslado y la eliminación inmediata de capítulos vacíos. Nuevos textos en
es/en/ca/fr; las cantidades se presentan como «Conceptos afectados: N» para evitar concordancias incorrectas con uno. QuoteChapterEditor.test.tsxverifica separación de acciones, destino obligatorio, transferencia conservando valores y eliminación exclusiva de conceptos sin borrar sus capítulos; se adaptan los textos de las confirmaciones existentes. Validación: 10 pruebas acotadas (cinco del editor y cinco de traducciones), TypeScript sin emisión, ESLint sin errores, Tailwind hacia/tmpygit diff --check.- Chromium ejecuta componente, formulario y selectores reales en un bundle local sin API a 1440 y 375 px. Verifica separación de bloques, ausencia de desbordamiento horizontal, desactivación sin destino, traslado preservando conceptos/precios y conservación sin agrupar al quitar el último capítulo. Muestras
/tmp/uecko-delete-chapter-*.png; no se incorporan fixtures ni dependencias al repositorio. - No se ejecutan PDF, base de datos, build de distribución ni E2E autenticado: cambia la presentación de decisiones existentes en el editor. Persisten los avisos previos de paginación fuera de
acty Browserslist desactualizado.
Ampliación: estado vacío para cotizaciones sin líneas ni capítulos
QuoteItemsSortableDataTable.tsxmuestraEmptycuando no hay detalles, en lugar de cabeceras y tabla sin contenido. Mensaje, icono neutro y botones para añadir fila vacía, artículo del catálogo o bloque de texto; usa las mismas acciones de tabla que la barra existente. El pie de botones se oculta mientras está vacío para no duplicar las acciones del propioEmpty.- Al insertar el primer concepto aparece la tabla normal, sin crear capítulos. El estado también se aplica cuando una cotización vuelve a quedarse sin conceptos. Las cotizaciones con capítulos conservan sus estados vacíos propios. Nuevos textos
quotes.empty_detailsenes/en/ca/fr. QuoteChapterEditor.test.tsxincorpora un formulario inicialmente vacío y tres pruebas de alta desdeEmpty, incluyendo los dos selectores y conservación del modo sin capítulos. Validación: tres pruebas nuevas y cinco de traducciones, tres casos existentes de tabla libre/orden de columnas, TypeScript sin emisión, ESLint acotado sin errores (cuatro avisosanyprevios), Tailwind hacia/tmpygit diff --check.- Chromium sobre el HTML del harness revisa a 1440 y 375 px la ausencia de tabla vacía, los tres botones, ajuste al ancho y ausencia de desbordamiento. Muestras
/tmp/uecko-new-empty-*.png; fixture temporal retirado. No se ejecutan PDF, base de datos, build de distribución ni E2E autenticado al limitarse al editor.