- Implemented CreateRecurringProformaFromProformaDialog component for creating recurring proformas from existing proformas. - Added unit tests for the new dialog and its integration within the proforma detail page. - Created API functions for handling the creation of recurring proformas. - Developed hooks to manage the creation process and invalidate relevant queries upon success. - Enhanced recurring proforma detail page to display relevant information and actions. - Added utility functions for formatting recurring proforma schedules. - Introduced layout components for better organization of recurring proforma UI.
195 lines
8.6 KiB
Markdown
195 lines
8.6 KiB
Markdown
# Diseño de cuadros de diálogo de formulario
|
|
|
|
## Estado
|
|
|
|
Este documento es la fuente de verdad para diseñar, implementar y revisar cuadros de diálogo que recogen o modifican datos en el frontend del ERP.
|
|
|
|
Todo diálogo nuevo de creación, edición o configuración debe seguir esta guía, salvo que una necesidad funcional justifique y documente una excepción.
|
|
|
|
## Referencia canónica
|
|
|
|
La referencia productiva es el diálogo para crear una proforma recurrente desde una proforma:
|
|
|
|
- componente: `modules/customer-invoices/src/web/proformas/detail/ui/components/create-recurring-proforma-from-proforma-dialog.tsx`;
|
|
- controller: `modules/customer-invoices/src/web/proformas/detail/controllers/use-create-recurring-proforma-from-proforma-dialog-controller.ts`;
|
|
- prueba de renderizado: `modules/customer-invoices/src/web/proformas/detail/ui/components/create-recurring-proforma-from-proforma-dialog.test.tsx`.
|
|
|
|
La guía no obliga a reutilizar ese componente. Obliga a mantener su jerarquía visual, separación de responsabilidades y comportamiento accesible.
|
|
|
|
## Cuándo aplicar este patrón
|
|
|
|
Usar `Dialog` para una tarea breve y focalizada que:
|
|
|
|
- se inicia desde la pantalla actual;
|
|
- requiere pocos datos para completar una única acción;
|
|
- no necesita una ruta propia ni navegación lateral;
|
|
- permite volver al contexto sin perder orientación.
|
|
|
|
No usarlo para:
|
|
|
|
- confirmaciones o acciones destructivas sin formulario: usar `AlertDialog`;
|
|
- ediciones documentales complejas, varios pasos o trabajo prolongado: usar una página de edición;
|
|
- vistas de solo lectura: usar un diálogo de consulta o el patrón existente del área.
|
|
|
|
## Estructura visual obligatoria
|
|
|
|
Un diálogo de formulario se organiza de arriba abajo en cuatro zonas:
|
|
|
|
1. Cabecera diferenciada.
|
|
2. Campo de identidad destacado cuando exista, normalmente el nombre.
|
|
3. Bloques de información agrupados por intención.
|
|
4. Pie de acciones separado del contenido.
|
|
|
|
```tsx
|
|
<DialogContent className="gap-0 overflow-hidden p-0 sm:max-w-2xl">
|
|
<DialogHeader className="border-b bg-muted/40 px-6 py-5 sm:px-7">
|
|
<DialogTitle>{title}</DialogTitle>
|
|
<DialogDescription className="max-w-lg">{description}</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form className="space-y-6 px-6 py-5 sm:px-7">
|
|
<IdentityField />
|
|
<InformationSection />
|
|
<ConfigurationSection />
|
|
</form>
|
|
|
|
<DialogFooter className="border-t bg-muted/30 px-6 py-4 sm:px-7">
|
|
<CancelAction />
|
|
<SubmitAction />
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
```
|
|
|
|
No añadir una zona si no aporta información o una acción real. La estructura debe mejorar la lectura, no convertir un formulario pequeño en una pantalla artificialmente compleja.
|
|
|
|
## 1. Cabecera diferenciada
|
|
|
|
La cabecera comunica qué se va a hacer antes de pedir datos.
|
|
|
|
- Incluir título directo, orientado a la acción: `Crear plazo de pago`, `Modificar método de pago`.
|
|
- Incluir una descripción breve que explique el resultado o el contexto de origen.
|
|
- Separarla visualmente del formulario mediante borde inferior y fondo sutil del tema (`bg-muted/40`).
|
|
- Mantener los márgenes internos alineados con contenido y pie.
|
|
- No colocar acciones de formulario en la cabecera; el cierre estándar del diálogo se mantiene disponible.
|
|
|
|
## 2. Campo de identidad destacado
|
|
|
|
Cuando el recurso tenga un nombre, código o referencia que ayude a reconocerlo, ese campo va inmediatamente después de la cabecera.
|
|
|
|
- Usar el nombre como primer campo si es obligatorio para crear o identificar el recurso.
|
|
- Dar al control una presencia moderada (`h-11` y fondo de formulario) sin crear un segundo título.
|
|
- Mantener el error de validación junto al campo.
|
|
- No destacar campos técnicos, identificadores internos ni valores que el usuario no pueda modificar.
|
|
|
|
Si el diálogo no tiene un campo de identidad relevante, el primer bloque de información pasa a ocupar esa posición.
|
|
|
|
## 3. Bloques de información
|
|
|
|
Agrupar campos que responden a la misma pregunta del usuario. Por ejemplo: periodo de vigencia, recurrencia, condiciones comerciales o destinatario.
|
|
|
|
Cada bloque debe:
|
|
|
|
- usar un `section` con `aria-labelledby`;
|
|
- tener un título visible y conciso;
|
|
- mostrar un icono solo cuando ayude a identificar el tipo de información;
|
|
- usar borde, radio y fondo sutil para delimitarlo sin competir con la acción principal;
|
|
- contener entre uno y seis controles relacionados por norma general.
|
|
|
|
Patrón base:
|
|
|
|
```tsx
|
|
<section aria-labelledby={sectionId} className="rounded-xl border bg-muted/20 p-4 sm:p-5">
|
|
<div className="mb-4 flex items-center gap-3">
|
|
<span className="flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
<SectionIcon aria-hidden className="size-4" />
|
|
</span>
|
|
<h3 className="font-medium text-sm" id={sectionId}>{sectionTitle}</h3>
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<Field />
|
|
<Field />
|
|
</div>
|
|
</section>
|
|
```
|
|
|
|
Reglas de composición:
|
|
|
|
- En escritorio, los campos de igual importancia pueden compartir dos columnas.
|
|
- En móvil, las columnas deben apilarse de forma natural. Usar `sm:grid-cols-2`, nunca una anchura fija.
|
|
- Añadir `min-w-0` a las columnas cuando puedan contener textos localizados, selectores o controles flexibles.
|
|
- Los selectores e inputs ocupan todo el ancho de su columna.
|
|
- Un campo condicional se muestra dentro del bloque al que pertenece y se separa con borde superior si necesita distinguirse del resto.
|
|
- Los interruptores y checkboxes que afectan a otro campo deben vivir junto a ese campo, no en una fila independiente sin contexto.
|
|
|
|
## 4. Pie de acciones
|
|
|
|
El pie mantiene las decisiones finales fuera de los datos.
|
|
|
|
- Separarlo mediante borde superior y fondo sutil (`bg-muted/30`).
|
|
- Colocar `Cancelar` como acción secundaria y la acción de envío como botón principal.
|
|
- Durante el envío, deshabilitar ambos botones y todos los controles editables.
|
|
- El botón principal debe reflejar el estado de progreso con texto o icono accesible.
|
|
- Asociar el botón de envío al `form` mediante su atributo `form` cuando el pie quede fuera del elemento `form`.
|
|
|
|
## Accesibilidad, validación e i18n
|
|
|
|
- Usar `DialogTitle` y `DialogDescription` siempre que el diálogo tenga contenido operativo.
|
|
- Asociar cada `Label` a un control mediante `htmlFor` e `id`.
|
|
- Los títulos de bloque usan `section` y `aria-labelledby`.
|
|
- Mantener los errores junto al control que los genera; no depender únicamente de avisos globales.
|
|
- Todo texto visible procede de i18n y debe existir en `es` y `en`.
|
|
- Los iconos decorativos usan `aria-hidden`; nunca sustituyen una etiqueta textual.
|
|
- No bloquear el cierre por teclado salvo mientras exista una operación de envío en curso.
|
|
|
|
## Responsabilidades del código
|
|
|
|
El componente de UI compone el diálogo y recibe estado ya preparado. No debe contener navegación, peticiones HTTP ni reglas de negocio.
|
|
|
|
El controller del diálogo concentra:
|
|
|
|
- valores iniciales y reinicio del formulario;
|
|
- validación de formulario;
|
|
- llamada al hook de mutación;
|
|
- estado de envío;
|
|
- cierre, notificaciones y navegación tras un resultado correcto.
|
|
|
|
Las operaciones remotas permanecen en hooks de TanStack Query. Los DTOs de petición siguen siendo la fuente de verdad del formulario y de la validación.
|
|
|
|
## Pruebas mínimas
|
|
|
|
Cada diálogo nuevo o modificado debe cubrir, como mínimo:
|
|
|
|
- título, descripción y campo de identidad cuando exista;
|
|
- campos obligatorios y campos condicionales;
|
|
- acciones de cancelar y enviar;
|
|
- estado de envío;
|
|
- traducciones reales o una prueba que compruebe que las claves usadas existen en el catálogo;
|
|
- resultado correcto y error de la mutación en el controller.
|
|
|
|
No usar un diccionario de traducciones manual en el test si el objetivo es verificar que el catálogo productivo contiene las claves consumidas por el componente.
|
|
|
|
## Lista de comprobación
|
|
|
|
Antes de cerrar un cambio, confirmar:
|
|
|
|
- La tarea cabe realmente en un diálogo.
|
|
- La cabecera explica la acción y está diferenciada del formulario.
|
|
- El nombre o campo de identidad aparece antes de los bloques de configuración.
|
|
- Cada bloque agrupa una única intención comprensible.
|
|
- El layout se apila correctamente en móvil.
|
|
- Los textos, estados disabled, errores y carga son accesibles.
|
|
- Existen traducciones en `es` y `en`.
|
|
- Las pruebas y la validación de formato se han ejecutado desde Linux.
|
|
|
|
## Excepciones
|
|
|
|
Una excepción debe justificarse en la revisión del cambio indicando:
|
|
|
|
- qué requisito funcional impide usar esta estructura;
|
|
- qué patrón alternativo se emplea;
|
|
- cómo se mantiene la accesibilidad y el comportamiento responsive;
|
|
- qué pruebas cubren la variante.
|
|
|
|
La preferencia visual local, copiar un diálogo antiguo o ahorrar un bloque de composición no son motivos válidos para omitir esta guía.
|