feat: refactor account and proforma pages to use app-fullscreen layout and improve UI consistency

This commit is contained in:
David Arranz 2026-08-18 11:54:45 +02:00
parent 64b90888c3
commit 4411b8f07e
10 changed files with 546 additions and 237 deletions

View File

@ -0,0 +1,274 @@
# Auditoría De Layouts De Editores
Fecha de auditoría: 18 de agosto de 2026.
## Objetivo
Inventariar los editores reales del frontend del ERP y clasificarlos respecto a la norma visual vigente de edición focalizada.
Esta auditoría:
- inventaría rutas, páginas y dialogs de edición activos;
- clasifica cada caso por tipo;
- evalúa el layout actual frente a la norma;
- detecta desviaciones y excepciones justificadas;
- propone una secuencia pequeña de migración.
Esta auditoría no migra masivamente las pantallas.
## Norma visual de referencia
SSOT vigente:
- `packages/rdx-ui/docs/page-layouts.md`
- `packages/rdx-ui/docs/contributing-ui.md`
Resumen operativo:
- los editores de página completa deben usar por defecto shell `app-fullscreen`;
- la navegación mínima sale por `PageFormHeader` o un header de dominio equivalente;
- el contenido del editor se centra mediante `RdxEditorPageLayout`;
- `complexity="simple"` aplica a formularios focalizados centrados;
- `complexity="complex"` aplica a documentos o editores con `main + aside`, bloques especializados y footer sticky;
- los dialogs e inline editors no deben migrarse automáticamente a fullscreen.
## Método de investigación
Búsquedas y fuentes revisadas:
- rutas y layouts en `apps/web/src/routes/app-routes.tsx` y `modules/*/src/web/*routes.tsx`
- contratos de layout en `modules/core/src/web/lib/modules/module-client.interface.ts`
- primitivas Rdx en:
- `packages/rdx-ui/src/components/editor-page/rdx-editor-page-layout.tsx`
- `packages/rdx-ui/src/components/editor-page/rdx-editor-sticky-footer.tsx`
- `modules/core/src/web/components/form/form-actions/page-form-header.tsx`
- `apps/web/src/layout/app-fullscreen-layout.tsx`
- `apps/web/src/layout/app-sidebar-layout.tsx`
- módulos revisados:
- `identity`
- `customers`
- `customer-invoices`
- `companies`
- `catalogs`
- `document-series`
- `supplier`
- búsquedas textuales sobre:
- `layout: "app-fullscreen"`
- `layout: "app-sidebar"`
- `RdxEditorPageLayout`
- `PageFormHeader`
- `RdxEditorStickyFooter`
- `create`
- `update`
- `edit`
- `Dialog`
## Inventario
| Module | Feature | Route | Type | Current layout | Rdx editor layout | Compliance | Risk | Priority | Recommended action | Evidence |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `identity` | Account self-service | `/account` | `PAGE_SIMPLE` | `app-fullscreen` | `simple` | `COMPLIANT` | `LOW` | `-` | Ninguna. Mantener como referencia de editor simple focalizado. | `modules/identity/src/web/account-routes.tsx`, `modules/identity/src/web/account/ui/pages/account-page.tsx` |
| `identity` | Change password dialog | Sin ruta propia. Dialog dentro de `/account` | `DIALOG` | Host `app-fullscreen` | No aplica | `COMPLIANT` | `LOW` | `-` | Ninguna. Mantener como dialog de edición contenida. | `modules/identity/src/web/account/ui/pages/account-page.tsx` |
| `customers` | Create customer | `/customers/create` | `PAGE_SIMPLE` | `app-fullscreen` | `simple` | `COMPLIANT` | `LOW` | `-` | Ninguna. Referencia vigente de editor simple. | `modules/customers/src/web/customer-routes.tsx`, `modules/customers/src/web/create/ui/pages/customer-create-page.tsx` |
| `customers` | Update customer | `/customers/:id/edit` | `PAGE_SIMPLE` | `app-fullscreen` | `simple` | `COMPLIANT` | `LOW` | `-` | Ninguna. Referencia vigente de editor simple. | `modules/customers/src/web/customer-routes.tsx`, `modules/customers/src/web/update/ui/pages/customer-update-page.tsx` |
| `customers` | Quick create customer | Sin ruta propia. Dialog reutilizado desde proformas | `DIALOG` | Host `app-fullscreen` | No aplica | `COMPLIANT` | `LOW` | `-` | Ninguna. Mantenerlo como creación rápida embebida. | `modules/customers/src/common/features/quick-create-customer/ui/dialogs/quick-create-customer-dialog.tsx` |
| `companies` | Company details settings | `/settings/company/details` | `PAGE_SIMPLE` | `app-sidebar` | No usa `RdxEditorPageLayout` | `EXCEPTION_JUSTIFIED` | `MEDIUM` | `P3` | Mantener en settings por ahora. Revaluar solo si se define una variante focalizada específica para formularios de settings. | `modules/companies/src/web/company-routes.tsx`, `modules/companies/src/web/settings/company-details/ui/pages/company-details-settings-page.tsx` |
| `customer-invoices` | Verifactu settings | `/settings/billing/verifactu` | `PAGE_SIMPLE` | `app-sidebar` | No usa `RdxEditorPageLayout` | `EXCEPTION_JUSTIFIED` | `MEDIUM` | `P3` | Mantener en settings por ahora. Revaluar junto con una política específica de formularios largos dentro de settings. | `modules/customer-invoices/src/web/customer-invoice-routes.tsx`, `modules/customer-invoices/src/web/settings/verifactu/ui/pages/company-verifactu-settings-page.tsx` |
| `customer-invoices` | Create proforma | `/proformas/create` | `PAGE_SIMPLE` | `app-fullscreen` | `simple` | `COMPLIANT` | `MEDIUM` | `-` | Ninguna. Mantener como bootstrap inicial de proforma hacia el editor documental completo. | `modules/customer-invoices/src/web/customer-invoice-routes.tsx`, `modules/customer-invoices/src/web/proformas/create/ui/pages/proforma-create-page.tsx`, `modules/customer-invoices/src/web/proformas/create/ui/blocks/proforma-create-form.tsx` |
| `customer-invoices` | Update proforma | `/proformas/:id/edit` | `PAGE_COMPLEX` | `app-fullscreen` | `complex` | `COMPLIANT` | `HIGH` | `-` | Ninguna. Mantener como referencia vigente de editor complejo. | `modules/customer-invoices/src/web/customer-invoice-routes.tsx`, `modules/customer-invoices/src/web/proformas/update/ui/pages/proforma-update-page.tsx`, `modules/customer-invoices/src/web/proformas/update/ui/editors/proforma-update-editor-form.tsx` |
| `customer-invoices` | Change proforma status | Sin ruta propia. Dialog usado desde listado y detalle | `DIALOG` | Host mixto (`app-sidebar` o `app-fullscreen`) | No aplica | `COMPLIANT` | `LOW` | `-` | Ninguna. Es una edición contenida por dialog, no candidata a fullscreen. | `modules/customer-invoices/src/web/proformas/change-status/ui/change-status-dialog.tsx`, `modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx`, `modules/customer-invoices/src/web/proformas/detail/ui/pages/proforma-detail-page.tsx` |
| `catalogs` | Payment methods create/update | Host: `/settings/general/payment-methods` | `DIALOG` | Host `app-sidebar` | No aplica | `COMPLIANT` | `LOW` | `-` | Ninguna. Mantener como edición contenida en dialog dentro de settings. | `modules/catalogs/src/web/catalogs-routes.tsx`, `modules/catalogs/src/web/settings/payment-methods/ui/pages/payment-methods-settings-page.tsx`, `modules/catalogs/src/web/settings/payment-methods/ui/components/payment-method-form-dialog.tsx` |
| `catalogs` | Payment terms create/update | Host: `/settings/general/payment-terms` | `DIALOG` | Host `app-sidebar` | No aplica | `COMPLIANT` | `LOW` | `-` | Ninguna. Mantener como dialog aunque su contenido interno sea más rico. | `modules/catalogs/src/web/catalogs-routes.tsx`, `modules/catalogs/src/web/settings/payment-terms/ui/pages/payment-terms-settings-page.tsx`, `modules/catalogs/src/web/settings/payment-terms/ui/components/payment-term-form-dialog.tsx` |
| `catalogs` | Tax regimes create/update | Host: `/settings/general/tax-regimes` | `DIALOG` | Host `app-sidebar` | No aplica | `COMPLIANT` | `LOW` | `-` | Ninguna. Mantener como dialog de edición contenida. | `modules/catalogs/src/web/catalogs-routes.tsx`, `modules/catalogs/src/web/settings/tax-regimes/ui/pages/tax-regimes-settings-page.tsx`, `modules/catalogs/src/web/settings/tax-regimes/ui/components/tax-regime-form-dialog.tsx` |
| `customer-invoices` | Issued invoices create/edit references | Referencias a `/issued-invoices/create` y `/customer-invoices/:id/edit`, sin ruta activa | `UNCLEAR` | Sin ruta activa | No aplica | `UNKNOWN` | `MEDIUM` | `P2` | Aclarar si esas rutas deben existir o limpiar referencias muertas antes de cualquier trabajo visual. | `modules/customer-invoices/src/web/issued-invoices/list/ui/pages/list-issued-invoices-page.tsx`, `modules/customer-invoices/src/web/issued-invoices/list/ui/blocks/issued-invoices-grid/issued-invoices-grid.tsx`, `modules/customer-invoices/src/web/customer-invoice-routes.tsx` |
| `document-series` | Frontend editor activo | No detectado | `LEGACY_OR_UNUSED` | Sin route frontend | No aplica | `UNKNOWN` | `LOW` | `P3` | Confirmar si el módulo tendrá UI activa antes de cualquier migración de layout. | Sin `src/web/*routes.tsx` ni páginas frontend activas en `modules/document-series` |
| `supplier` | Frontend editor activo | No detectado | `LEGACY_OR_UNUSED` | Sin route frontend | No aplica | `UNKNOWN` | `LOW` | `P3` | Resolver primero si el módulo se reactiva o se limpia; no priorizar migración visual. | Sin `src/web/*routes.tsx`; solo `modules/supplier/src/web/shared/*` |
## Editores conformes
Conformes respecto a la norma vigente:
- `customers/create`
- `customers/update`
- `/account`
- `proformas/:id/edit`
Motivos comunes observados:
- ruta bajo `app-fullscreen`;
- navegación mínima por `PageFormHeader` o header de dominio equivalente;
- uso explícito de `RdxEditorPageLayout`;
- geometría coherente con `simple` o `complex` según caso;
- acciones de salida/guardado visibles y no delegadas al sidebar.
## Desviaciones
### 1. `proformas/create`
HECHO OBSERVADO
- la ruta usa `app-fullscreen`;
- la página usa header de edición y `RdxEditorPageLayout complexity="simple"`;
- el flujo de alta actual solo captura cabecera inicial y deriva después al editor documental completo;
- no existe todavía editor de líneas, `aside`, totales sticky ni footer persistente equivalentes a `proformas/:id/edit`.
IMPACTO
- el create queda alineado con el patrón focalizado simple sin fingir que ya es el editor documental completo;
- la diferencia visual restante frente a `update` responde a una diferencia funcional real y no a drift accidental de layout.
DECISIÓN
- `PAGE_SIMPLE`;
- `COMPLIANT`.
### 2. Referencias a create/edit de issued invoices sin ruta activa
HECHO OBSERVADO
- hay referencias a `/issued-invoices/create` y a `/customer-invoices/:id/edit`;
- `customer-invoice-routes.tsx` no declara esas rutas activas.
IMPACTO
- el inventario de editores queda ambiguo en esa área;
- puede existir deuda de navegación muerta o una funcionalidad incompleta;
- no conviene planificar migración visual ahí hasta aclarar si hay editor real.
RECOMENDACIÓN
- confirmar si esas rutas deben implementarse;
- si no, limpiar referencias muertas antes de meterlas en cualquier backlog de layouts.
## Excepciones justificadas
### Company details settings
HECHO OBSERVADO
- es un formulario de página completa;
- vive bajo `/settings/company/details`;
- depende del shell de settings con `app-sidebar`.
INFERENCIA
- la navegación lateral de settings forma parte funcional de la tarea y hoy actúa como mecanismo primario de cambio de subsección.
DECISIÓN DE AUDITORÍA
- `EXCEPTION_JUSTIFIED` por ahora;
- no priorizar fullscreen salvo que el sistema de settings evolucione con una variante focalizada explícita.
### Verifactu settings
HECHO OBSERVADO
- es un formulario de configuración largo;
- vive bajo `/settings/billing/verifactu`;
- comparte patrón de settings con shell lateral.
INFERENCIA
- igual que en `company/details`, la tarea sigue integrada en navegación de settings y no en un editor aislado.
DECISIÓN DE AUDITORÍA
- `EXCEPTION_JUSTIFIED` por ahora;
- revisar sólo si se aborda una política transversal de formularios largos dentro de settings.
## Dialogs e inline editors
Dialogs correctamente clasificados y no candidatos automáticos a fullscreen:
- `QuickCreateCustomerDialog`
- `PaymentMethodFormDialog`
- `PaymentTermFormDialog`
- `TaxRegimeFormDialog`
- dialog de cambio de contraseña de `/account`
- `ChangeProformaStatusDialog`
No se detectaron rutas independientes que deban reclasificarse desde `DIALOG` o `INLINE` a `PAGE_*`.
No se detectaron inline editors de entidad de primer nivel con route propia. Sí existen bloques especializados internos, como el editor de líneas de proformas, pero forman parte de un `PAGE_COMPLEX` y no deben inventariarse como pantalla independiente.
## Incoherencias create/update
Agregados revisados:
- `customers`
- `create` y `update` comparten patrón y están alineados.
- `proformas`
- `create` usa `RdxEditorPageLayout complexity="simple"` porque hoy es un bootstrap inicial;
- `update` usa `RdxEditorPageLayout complexity="complex"` porque ya es el editor documental completo con `main`, bloques especializados y footer sticky.
Conclusión:
- la diferencia create/update restante es funcional legítima: `create` todavía no edita el documento completo y `update` sí.
## Riesgos de migración
Riesgos bajos:
- `customers/create`
- `customers/update`
- `/account`
Riesgos medios:
- `proformas/create` por convivencia con quick-create/select customer dialogs y porque el flujo actual parece de bootstrap inicial, no de edición documental completa;
- `company/details` y `verifactu` si se intentara moverlos fuera del shell settings.
Riesgos altos:
- `proformas/:id/edit`, aunque hoy ya cumple. Cualquier cambio allí impacta:
- scroll complejo
- footer sticky
- totales embebidos
- `aside`
- dialogs anidados
## Plan incremental
### Fase A
- completada: migración de `proformas/create`
Motivo:
- era el único `PAGE_*` activo no plenamente alineado con el patrón Rdx;
- ya usaba `app-fullscreen`, así que el salto era incremental;
- permite distinguir mejor entre bootstrap inicial y editor documental completo del agregado Proforma.
### Fase B
- revisar si `company/details` y `verifactu` deben seguir como excepción de settings o si merece una variante focalizada específica para formularios largos dentro de settings
Motivo:
- ahora mismo la excepción parece razonable;
- no conviene forzar fullscreen sin una decisión explícita sobre el sistema de settings.
### Fase C
- aclarar `issued-invoices` create/edit
Motivo:
- primero hay que determinar si existe editor real o solo referencias muertas.
### Fase D
- hacer una auditoría UX más fina de dialogs ricos, empezando por `payment-terms`
Motivo:
- no es un incumplimiento de fullscreen;
- sí es el dialog más complejo de los auditados y puede requerir reglas específicas de consistencia interna.
## No cambios
Durante esta auditoría:
- no se modificaron rutas ni layouts runtime de `customers`, `companies`, `customer-invoices`, `catalogs`, `identity`, `document-series` o `supplier`;
- no se tocaron hooks, controllers, formularios, React Query ni backend de los editores auditados;
- el único cambio de código del turno es este documento operativo de auditoría.

View File

@ -1,6 +1,5 @@
import type { CustomerSelectionOption } from "@erp/customers";
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { ProformaCreateAutoAppliedInfo } from "./proforma-create-auto-applied-info";
import { ProformaCreateEmptyLinesCard } from "./proforma-create-empty-lines-card";
@ -20,7 +19,6 @@ interface ProformaCreateFormProps {
proformaSeriesOptions: { value: string; label: string }[];
targetInvoiceSeriesOptions: { value: string; label: string }[];
paymentTermOptions: { value: string; label: string }[];
className?: string;
}
export const ProformaCreateForm = ({
@ -37,31 +35,28 @@ export const ProformaCreateForm = ({
proformaSeriesOptions,
targetInvoiceSeriesOptions,
paymentTermOptions,
className,
}: ProformaCreateFormProps) => {
return (
<div className={cn("p-6", className)}>
<form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}>
<div className="space-y-6">
<ProformaCreateInitialInfoCard
disabled={isSubmitting}
isPaymentTermsLoading={isPaymentTermsLoading}
isProformaSeriesLoading={isProformaSeriesLoading}
isTargetInvoiceSeriesLoading={isTargetInvoiceSeriesLoading}
onClearCustomer={onClearCustomer}
onPaymentTermChange={onPaymentTermChange}
onSelectCustomer={onSelectCustomer}
paymentTermOptions={paymentTermOptions}
proformaSeriesOptions={proformaSeriesOptions}
selectedCustomer={selectedCustomer}
targetInvoiceSeriesOptions={targetInvoiceSeriesOptions}
/>
<form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}>
<div className="space-y-6">
<ProformaCreateInitialInfoCard
disabled={isSubmitting}
isPaymentTermsLoading={isPaymentTermsLoading}
isProformaSeriesLoading={isProformaSeriesLoading}
isTargetInvoiceSeriesLoading={isTargetInvoiceSeriesLoading}
onClearCustomer={onClearCustomer}
onPaymentTermChange={onPaymentTermChange}
onSelectCustomer={onSelectCustomer}
paymentTermOptions={paymentTermOptions}
proformaSeriesOptions={proformaSeriesOptions}
selectedCustomer={selectedCustomer}
targetInvoiceSeriesOptions={targetInvoiceSeriesOptions}
/>
<ProformaCreateAutoAppliedInfo />
<ProformaCreateAutoAppliedInfo />
<ProformaCreateEmptyLinesCard />
</div>
</form>
</div>
<ProformaCreateEmptyLinesCard />
</div>
</form>
);
};

View File

@ -1,6 +1,7 @@
import { ErrorAlert } from "@erp/core/components";
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
import { QuickCreateCustomerDialog, SelectCustomerDialog } from "@erp/customers";
import { RdxEditorPageLayout } from "@repo/rdx-ui/components";
import { FormProvider } from "react-hook-form";
import { ProformaInfoAlert } from "../../../update/ui/components";
@ -29,44 +30,50 @@ export const ProformaCreatePage = () => {
</ProformaInfoAlert>
</ProformaCreateHeader>
{createCtrl.isCreateError && (
<ErrorAlert
message={
createCtrl.createError instanceof Error
? createCtrl.createError.message
: "Revisa los datos e inténtalo de nuevo."
}
title="No se pudo crear la proforma"
/>
)}
<div className="flex-1 overflow-y-auto bg-muted/20">
<RdxEditorPageLayout complexity="simple">
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
<div className="flex min-w-0 flex-col gap-6">
{createCtrl.isCreateError && (
<ErrorAlert
message={
createCtrl.createError instanceof Error
? createCtrl.createError.message
: "Revisa los datos e inténtalo de nuevo."
}
title="No se pudo crear la proforma"
/>
)}
{createCtrl.isSeriesLoadError && (
<ErrorAlert
message={
createCtrl.seriesLoadError instanceof Error
? createCtrl.seriesLoadError.message
: "Inténtalo de nuevo más tarde."
}
title="No se pudieron cargar las series de facturación"
/>
)}
{createCtrl.isSeriesLoadError && (
<ErrorAlert
message={
createCtrl.seriesLoadError instanceof Error
? createCtrl.seriesLoadError.message
: "Inténtalo de nuevo más tarde."
}
title="No se pudieron cargar las series de facturación"
/>
)}
<div className="flex-1 overflow-y-auto">
<ProformaCreateForm
formId={createCtrl.formId}
isPaymentTermsLoading={createCtrl.isPaymentTermsLoading}
isProformaSeriesLoading={createCtrl.isProformaSeriesLoading}
isSubmitting={createCtrl.isCreating}
isTargetInvoiceSeriesLoading={createCtrl.isTargetInvoiceSeriesLoading}
onClearCustomer={createCtrl.clearCustomer}
onPaymentTermChange={createCtrl.handlePaymentTermChange}
onSelectCustomer={selectCustomerCtrl.selectCtrl.openDialog}
onSubmit={createCtrl.onSubmit}
paymentTermOptions={createCtrl.paymentTermOptions}
proformaSeriesOptions={createCtrl.proformaSeriesOptions}
selectedCustomer={createCtrl.selectedCustomer}
targetInvoiceSeriesOptions={createCtrl.targetInvoiceSeriesOptions}
/>
<ProformaCreateForm
formId={createCtrl.formId}
isPaymentTermsLoading={createCtrl.isPaymentTermsLoading}
isProformaSeriesLoading={createCtrl.isProformaSeriesLoading}
isSubmitting={createCtrl.isCreating}
isTargetInvoiceSeriesLoading={createCtrl.isTargetInvoiceSeriesLoading}
onClearCustomer={createCtrl.clearCustomer}
onPaymentTermChange={createCtrl.handlePaymentTermChange}
onSelectCustomer={selectCustomerCtrl.selectCtrl.openDialog}
onSubmit={createCtrl.onSubmit}
paymentTermOptions={createCtrl.paymentTermOptions}
proformaSeriesOptions={createCtrl.proformaSeriesOptions}
selectedCustomer={createCtrl.selectedCustomer}
targetInvoiceSeriesOptions={createCtrl.targetInvoiceSeriesOptions}
/>
</div>
</div>
</RdxEditorPageLayout>
</div>
<SelectCustomerDialog

View File

@ -82,12 +82,13 @@ Reglas actuales:
### UX actual de `/account`
La pantalla web `/account` se presenta como una única vista de self-service para el usuario autenticado.
La pantalla web `/account` se presenta como un editor focalizado de self-service para el usuario autenticado.
Contenido visible actual:
- información personal: `name`, `email`, `language_code`
- avatar en modo solo lectura, con fallback por iniciales
- modo `app-fullscreen`, sin sidebar principal
- contenido centrado con ancho controlado mediante `RdxEditorPageLayout`
- seguridad mediante dialog para cambio de contraseña
- cierre de sesión reutilizando el flujo de logout existente

View File

@ -1,5 +1,8 @@
{
"account": {
"header": {
"back": "Back"
},
"title": "My account",
"description": "Manage your personal details, preferences, and security.",
"loading": "Loading your account...",

View File

@ -1,5 +1,8 @@
{
"account": {
"header": {
"back": "Volver"
},
"title": "Mi cuenta",
"description": "Gestiona tus datos personales, preferencias y seguridad.",
"loading": "Cargando tu cuenta...",

View File

@ -13,7 +13,7 @@ export const IdentityAccountRoutes = (_params: ModuleClientParams): ModuleRouteO
{
path: "account",
handle: {
layout: "app-sidebar",
layout: "app-fullscreen",
protected: true,
requireCompany: false,
},

View File

@ -1,8 +1,8 @@
import { ErrorAlert, PageErrorState } from "@erp/core/components";
import { UnsavedChangesProvider } from "@erp/core/hooks";
import { ErrorAlert, PageFormHeader, PageErrorState } from "@erp/core/components";
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
import {
AppContent,
CancelActionButton,
RdxEditorPageLayout,
RdxEditorSection,
RdxEditorStickyFooter,
RhfSubmitActionButton,
@ -28,15 +28,15 @@ import type { AccountPasswordForm, AccountProfileForm } from "../../entities";
const AccountLoadingState = ({ label }: { label: string }) => {
return (
<AppContent className="py-2">
<div
aria-busy="true"
className="flex min-h-64 items-center justify-center text-sm text-muted-foreground"
role="status"
>
{label}
</div>
</AppContent>
<div className="flex-1 overflow-y-auto bg-muted/20">
<RdxEditorPageLayout className="min-h-full" complexity="simple">
<div className="flex min-h-[16rem] items-center justify-center text-sm text-muted-foreground">
<div aria-busy="true" role="status">
{label}
</div>
</div>
</RdxEditorPageLayout>
</div>
);
};
@ -62,25 +62,27 @@ const AccountErrorState = ({
variant: React.ComponentProps<typeof PageErrorState>["variant"];
}) => {
return (
<AppContent className="py-2">
<div className="mx-auto w-full max-w-4xl">
<PageErrorState
description={description}
primaryAction={
canRetry
? {
label: isRetrying ? retryingLabel : retryLabel,
onClick: onRetry,
isLoading: isRetrying,
}
: undefined
}
reference={reference}
title={title}
variant={variant}
/>
</div>
</AppContent>
<div className="flex-1 overflow-y-auto bg-muted/20">
<RdxEditorPageLayout complexity="simple">
<div className="mx-auto w-full max-w-3xl">
<PageErrorState
description={description}
primaryAction={
canRetry
? {
label: isRetrying ? retryingLabel : retryLabel,
onClick: onRetry,
isLoading: isRetrying,
}
: undefined
}
reference={reference}
title={title}
variant={variant}
/>
</div>
</RdxEditorPageLayout>
</div>
);
};
@ -200,6 +202,9 @@ export const AccountPage = () => {
const { t } = useTranslation();
const ctrl = useAccountPageController();
const [isPasswordDialogOpen, setPasswordDialogOpen] = useState(false);
const { navigateBack } = useReturnToNavigation({
fallbackPath: "/dashboard",
});
useEffect(() => {
if (!ctrl.passwordSuccessVersion) {
@ -246,144 +251,150 @@ export const AccountPage = () => {
};
return (
<AppContent className="py-2">
<div className="max-w-4xl space-y-6">
<FormProvider {...ctrl.profileForm}>
<UnsavedChangesProvider isDirty={ctrl.profileForm.formState.isDirty}>
<div className="space-y-6">
<div className="space-y-2">
<h1 className="text-2xl font-semibold tracking-tight">{t("account.title")}</h1>
<p className="max-w-3xl text-sm text-muted-foreground">
{t("account.description")}
</p>
</div>
<div className="fixed inset-0 flex flex-col overflow-hidden">
<PageFormHeader
backLabel={t("account.header.back")}
onBack={navigateBack}
title={t("account.title")}
/>
{ctrl.inlineProfileError ? (
<ErrorAlert
message={ctrl.inlineProfileError.description}
title={ctrl.inlineProfileError.title}
/>
) : null}
<div className="flex-1 overflow-y-auto bg-muted/20">
<RdxEditorPageLayout complexity="simple">
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
<div className="flex min-w-0 flex-col gap-6">
<FormProvider {...ctrl.profileForm}>
<UnsavedChangesProvider isDirty={ctrl.profileForm.formState.isDirty}>
<div className="space-y-6">
<p className="max-w-3xl text-sm text-muted-foreground">
{t("account.description")}
</p>
<form
className="flex flex-col gap-6"
id={ctrl.profileFormId}
noValidate
onKeyDown={ctrl.onFormKeyDown}
onSubmit={ctrl.onProfileSubmit}
>
<RdxEditorSection
description={t("account.sections.personal.description")}
title={t("account.sections.personal.title")}
variant="panel"
>
<div className="grid grid-cols-1 gap-x-6 gap-y-5 md:grid-cols-12">
<TextField<AccountProfileForm>
className="md:col-span-6"
disabled={ctrl.isSavingProfile}
inputClassName="bg-background"
label={t("account.sections.personal.fields.name.label")}
name="name"
placeholder={t("account.sections.personal.fields.name.placeholder")}
required
/>
{ctrl.inlineProfileError ? (
<ErrorAlert
message={ctrl.inlineProfileError.description}
title={ctrl.inlineProfileError.title}
/>
) : null}
<TextField<AccountProfileForm>
className="md:col-span-6"
disabled={ctrl.isSavingProfile}
inputClassName="bg-background"
label={t("account.sections.personal.fields.email.label")}
name="email"
placeholder={t("account.sections.personal.fields.email.placeholder")}
required
typePreset="email"
/>
<form
className="flex flex-col gap-6"
id={ctrl.profileFormId}
noValidate
onKeyDown={ctrl.onFormKeyDown}
onSubmit={ctrl.onProfileSubmit}
>
<RdxEditorSection
description={t("account.sections.personal.description")}
title={t("account.sections.personal.title")}
>
<div className="grid grid-cols-1 gap-x-6 gap-y-5 md:grid-cols-12">
<TextField<AccountProfileForm>
className="md:col-span-6"
disabled={ctrl.isSavingProfile}
inputClassName="bg-background"
label={t("account.sections.personal.fields.name.label")}
name="name"
placeholder={t("account.sections.personal.fields.name.placeholder")}
required
/>
<SelectField<AccountProfileForm>
className="md:col-span-12"
description={t("account.sections.personal.fields.languageCode.description")}
disabled={ctrl.isSavingProfile}
inputClassName="bg-background"
items={[
{
value: "es",
label: t("account.sections.personal.fields.languageCode.options.es"),
},
{
value: "en",
label: t("account.sections.personal.fields.languageCode.options.en"),
},
]}
label={t("account.sections.personal.fields.languageCode.label")}
name="languageCode"
placeholder={t("account.sections.personal.fields.languageCode.placeholder")}
<TextField<AccountProfileForm>
className="md:col-span-6"
disabled={ctrl.isSavingProfile}
inputClassName="bg-background"
label={t("account.sections.personal.fields.email.label")}
name="email"
placeholder={t("account.sections.personal.fields.email.placeholder")}
required
typePreset="email"
/>
<SelectField<AccountProfileForm>
className="md:col-span-12"
description={t("account.sections.personal.fields.languageCode.description")}
disabled={ctrl.isSavingProfile}
inputClassName="bg-background"
items={[
{
value: "es",
label: t("account.sections.personal.fields.languageCode.options.es"),
},
{
value: "en",
label: t("account.sections.personal.fields.languageCode.options.en"),
},
]}
label={t("account.sections.personal.fields.languageCode.label")}
name="languageCode"
placeholder={t("account.sections.personal.fields.languageCode.placeholder")}
/>
</div>
</RdxEditorSection>
</form>
<p aria-live="polite" className="sr-only">
{ctrl.isSavingProfile
? t("account.feedback.savingProfile")
: t("account.feedback.idle")}
</p>
<RdxEditorStickyFooter
primary={
<RhfSubmitActionButton
busyLabel={t("account.actions.savingProfile")}
disabled={ctrl.isSavingProfile || !ctrl.profileForm.formState.isDirty}
formId={ctrl.profileFormId}
isBusy={ctrl.profileForm.formState.isSubmitting}
label={t("account.actions.saveProfile")}
/>
}
secondary={
<CancelActionButton
disabled={ctrl.isSavingProfile || !ctrl.profileForm.formState.isDirty}
label={t("account.actions.resetProfile")}
onCancel={ctrl.resetProfileForm}
variant="outline"
/>
}
/>
</div>
</RdxEditorSection>
</form>
</UnsavedChangesProvider>
</FormProvider>
<p aria-live="polite" className="sr-only">
{ctrl.isSavingProfile
? t("account.feedback.savingProfile")
: t("account.feedback.idle")}
</p>
<RdxEditorSection
description={t("account.sections.security.description")}
title={t("account.sections.security.title")}
>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
<h2 className="text-base font-medium">{t("account.sections.security.passwordTitle")}</h2>
<p className="text-sm text-muted-foreground">
{t("account.sections.security.passwordDescription")}
</p>
</div>
<RdxEditorStickyFooter
primary={
<RhfSubmitActionButton
busyLabel={t("account.actions.savingProfile")}
disabled={ctrl.isSavingProfile || !ctrl.profileForm.formState.isDirty}
formId={ctrl.profileFormId}
isBusy={ctrl.profileForm.formState.isSubmitting}
label={t("account.actions.saveProfile")}
/>
}
secondary={
<CancelActionButton
disabled={ctrl.isSavingProfile || !ctrl.profileForm.formState.isDirty}
label={t("account.actions.resetProfile")}
onCancel={ctrl.resetProfileForm}
variant="outline"
/>
}
/>
<Button onClick={() => handlePasswordDialogOpenChange(true)} type="button">
{t("account.actions.changePassword")}
</Button>
</div>
</RdxEditorSection>
<RdxEditorSection
description={t("account.sections.session.description")}
title={t("account.sections.session.title")}
>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<p className="text-sm text-muted-foreground">
{t("account.sections.session.description")}
</p>
<Button onClick={ctrl.handleLogout} type="button" variant="outline">
{t("account.actions.logout")}
</Button>
</div>
</RdxEditorSection>
</div>
</UnsavedChangesProvider>
</FormProvider>
<RdxEditorSection
description={t("account.sections.security.description")}
title={t("account.sections.security.title")}
variant="panel"
>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
<h2 className="text-base font-medium">{t("account.sections.security.passwordTitle")}</h2>
<p className="text-sm text-muted-foreground">
{t("account.sections.security.passwordDescription")}
</p>
</div>
<Button onClick={() => handlePasswordDialogOpenChange(true)} type="button">
{t("account.actions.changePassword")}
</Button>
</div>
</RdxEditorSection>
<RdxEditorSection
description={t("account.sections.session.description")}
title={t("account.sections.session.title")}
variant="panel"
>
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<p className="text-sm text-muted-foreground">
{t("account.sections.session.description")}
</p>
<Button onClick={ctrl.handleLogout} type="button" variant="outline">
{t("account.actions.logout")}
</Button>
</div>
</RdxEditorSection>
</div>
</RdxEditorPageLayout>
</div>
<AccountPasswordDialog
@ -393,6 +404,6 @@ export const AccountPage = () => {
onOpenChange={handlePasswordDialogOpenChange}
t={t}
/>
</AppContent>
</div>
);
};

View File

@ -64,6 +64,8 @@ Deben recibir props ya preparadas por la capa consumidora.
## Editor simple
- por defecto, las pantallas cuya finalidad principal sea crear, editar o configurar mediante formulario deben usar shell `app-fullscreen` y no el shell con sidebar;
- usar `PageFormHeader` o el header de dominio ya existente para la navegación mínima de salida, sin duplicar una segunda navegación lateral;
- `RdxEditorSection` con `variant="plain"` es el patrón por defecto del editor simple;
- usar una única superficie editable cuando sea útil separar claramente la página del área de formulario;
- no envolver cada sección en `Card` por defecto;

View File

@ -101,6 +101,8 @@ scroll container
Reglas:
- las rutas cuya tarea principal sea crear, editar o configurar mediante un formulario focalizado deben entrar por el shell `app-fullscreen`;
- listados, dashboards, detalle y navegación de settings mantienen `app-sidebar` salvo necesidad funcional explícita;
- `RdxEditorPageLayout` resuelve padding interior, ancho centrado y `min-w-0`;
- el editor simple puede apoyarse en una única superficie contenedora para diferenciar la página del área editable, sin volver a cards por sección;
- `RdxEditorSection` expresa jerarquía por defecto con spacing y divisor inferior;
@ -109,22 +111,25 @@ Reglas:
- usar secciones planas con espacio o divisores;
- reservar sticky footer solo cuando el scroll lo justifique.
Ejemplo validado en `customers/update` y `customers/create`:
Ejemplo validado en `customers/update`, `customers/create` y `/account`:
```tsx
<div className="flex-1 overflow-y-auto">
<RdxEditorPageLayout complexity="simple">
<div className="flex min-w-0 flex-col gap-6">
{notice}
<AppFullscreenLayout>
<PageFormHeader backLabel={backLabel} onBack={onBack} title={title} />
<div className="flex-1 overflow-y-auto">
<RdxEditorPageLayout complexity="simple">
<div className="flex min-w-0 flex-col gap-6">
{notice}
<form className="flex min-w-0 flex-col gap-6">
<RdxEditorSection title={title} description={description}>
<RdxFormGrid>{fields}</RdxFormGrid>
</RdxEditorSection>
</form>
</div>
</RdxEditorPageLayout>
</div>
<form className="flex min-w-0 flex-col gap-6">
<RdxEditorSection title={title} description={description}>
<RdxFormGrid>{fields}</RdxFormGrid>
</RdxEditorSection>
</form>
</div>
</RdxEditorPageLayout>
</div>
</AppFullscreenLayout>
```
Deuda registrada:
@ -142,8 +147,16 @@ ESPECÍFICO
defaults iniciales
wiring de submit
feedback de error
navegación de retorno concreta
```
Candidatos explícitos para auditoría posterior:
- `customers/create`
- `customers/update`
- `/account`
- `proformas/update`
## Editor complejo
```txt