Refactor pages to use RdxPageContent for consistent layout

- Updated IssuedInvoiceDetailPage to wrap PageErrorState in RdxPageContent.
- Modified ProformaDetailPage to include RdxPageContent around PageErrorState.
- Adjusted RecurringProformaDetailPage to utilize RdxPageContent for loading and error states.
- Refactored CustomerViewPage to implement RdxPageContent for PageErrorState and main content.
- Renamed SupplierDetailPage to SupplierViewPage and updated imports accordingly.
- Removed SupplierDetailPage and created SupplierViewPage with similar functionality.
This commit is contained in:
David Arranz 2026-09-15 12:04:27 +02:00
parent ba257fd489
commit 902f7806ed
10 changed files with 390 additions and 355 deletions

View File

@ -82,7 +82,7 @@ RdxPageContent
→ composición de los hijos principales → composición de los hijos principales
``` ```
Regla: Las Pages no deben reconstruir estas responsabilidades con wrappers HTML locales. En Create/Update, la composición canónica es RdxPageFrame → RdxPageContainer → RdxPageContent: los errores inline y el editor son hijos directos de RdxPageContent; los dialogs son hermanos del frame; RdxPanel se reserva para secciones semánticas reales. Regla: Las Pages no deben reconstruir estas responsabilidades con wrappers HTML locales. En Create/Update y Detail/Show, la composición canónica es RdxPageFrame → RdxPageContainer → RdxPageContent: errores, alertas, secciones semánticas y contenido funcional son hijos directos de RdxPageContent; los dialogs son hermanos del frame; RdxPanel se reserva para secciones semánticas reales.
## 6. Identity y sesión ## 6. Identity y sesión

View File

@ -59,7 +59,7 @@ El error bloqueante conserva la misma jerarquía: RdxPageFrame → RdxPageContai
## 3. Detail ## 3. Detail
Un detalle normal usa `RdxPageFrame`, `RdxEditorHeader` cuando es fullscreen y el container apropiado. Un detalle normal usa `RdxPageFrame`, `RdxEditorHeader` cuando es fullscreen y la jerarquía `RdxPageContainer → RdxPageContent`; errores, alertas, secciones semánticas y contenido funcional viven dentro de `RdxPageContent`.
Un detalle documental especializado puede usar: Un detalle documental especializado puede usar:
@ -69,7 +69,7 @@ RdxPageFrame content overflow-hidden
└── inspector → scroll interno └── inspector → scroll interno
``` ```
Solo se usa esta excepción cuando los panes necesitan permanecer visibles simultáneamente. No se crea un layout genérico de detalle si no existe repetición real. Solo se usa esta excepción cuando los panes necesitan permanecer visibles simultáneamente. Su scroll interno es parte del workspace documental, no el scroll estructural de la Page. No se crea un layout genérico de detalle si no existe repetición real.
## 4. Settings ## 4. Settings

View File

@ -78,7 +78,7 @@ Compone header, contenido y footer y es propietario del scroll principal. No con
</RdxPageFrame> </RdxPageFrame>
``` ```
`contentClassName="overflow-hidden"` se reserva para layouts especializados con panes y scrolls internos. `contentClassName="overflow-hidden"` se reserva para layouts especializados con panes y scrolls internos; no desplaza la responsabilidad del scroll estructural de Page a wrappers locales.
### Headers ### Headers
@ -134,7 +134,7 @@ AppMain (espacio disponible, sin scroll)
Una Page no debe usar `fixed inset-0`, `h-screen`, `h-dvh`, `overflow-y-auto` ni `overflow-auto` para reconstruir viewport o scroll principal. Una Page no debe usar `fixed inset-0`, `h-screen`, `h-dvh`, `overflow-y-auto` ni `overflow-auto` para reconstruir viewport o scroll principal.
Excepción: un detalle documental puede configurar el contenido del frame con `overflow-hidden` y delegar scroll interno a document panes o inspectors. Dialogs, sheets, drawers, overlays, viewers y paneles internos también pueden necesitar posicionamiento o scroll propios. Excepción: un detalle documental puede configurar el contenido del frame con `overflow-hidden` y delegar scroll interno a document panes o inspectors. Ese scroll es funcional al workspace —por ejemplo, `ProformaDetailContent` e `IssuedInvoiceDetailContent`— y no sustituye el scroll estructural de Page. Dialogs, sheets, drawers, overlays, viewers y paneles internos también pueden necesitar posicionamiento o scroll propios.
## 7. Superficies ## 7. Superficies

View File

@ -1,5 +1,5 @@
import { PageErrorState } from "@erp/core/components"; import { PageErrorState } from "@erp/core/components";
import { RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components"; import { RdxPageContainer, RdxPageContent, RdxPageFrame } from "@repo/rdx-ui/components";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../i18n";
import { useIssuedInvoiceDetailPageController } from "../../controllers"; import { useIssuedInvoiceDetailPageController } from "../../controllers";
@ -23,14 +23,16 @@ export const IssuedInvoiceDetailPage = () => {
return ( return (
<RdxPageFrame> <RdxPageFrame>
<RdxPageContainer width="content"> <RdxPageContainer width="content">
<PageErrorState <RdxPageContent>
description={ctrl.state.pageError.description} <PageErrorState
primaryAction={ctrl.state.pageError.primaryAction} description={ctrl.state.pageError.description}
reference={ctrl.state.pageError.reference} primaryAction={ctrl.state.pageError.primaryAction}
secondaryAction={ctrl.state.pageError.secondaryAction} reference={ctrl.state.pageError.reference}
title={ctrl.state.pageError.title} secondaryAction={ctrl.state.pageError.secondaryAction}
variant={ctrl.state.pageError.variant} title={ctrl.state.pageError.title}
/> variant={ctrl.state.pageError.variant}
/>
</RdxPageContent>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame> </RdxPageFrame>
); );
@ -40,15 +42,17 @@ export const IssuedInvoiceDetailPage = () => {
return ( return (
<RdxPageFrame> <RdxPageFrame>
<RdxPageContainer width="content"> <RdxPageContainer width="content">
<PageErrorState <RdxPageContent>
description={t("pages.issued_invoices.detail.page_error.inconsistent.description")} <PageErrorState
secondaryAction={{ description={t("pages.issued_invoices.detail.page_error.inconsistent.description")}
label: t("pages.issued_invoices.detail.page_error.actions.back_to_list"), secondaryAction={{
onClick: ctrl.navigation.navigateBack, label: t("pages.issued_invoices.detail.page_error.actions.back_to_list"),
}} onClick: ctrl.navigation.navigateBack,
title={t("pages.issued_invoices.detail.page_error.unknown.title")} }}
variant="unknown" title={t("pages.issued_invoices.detail.page_error.unknown.title")}
/> variant="unknown"
/>
</RdxPageContent>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame> </RdxPageFrame>
); );

View File

@ -1,5 +1,5 @@
import { PageErrorState } from "@erp/core/components"; import { PageErrorState } from "@erp/core/components";
import { RdxPageFrame } from "@repo/rdx-ui/components"; import { RdxPageContainer, RdxPageContent, RdxPageFrame } from "@repo/rdx-ui/components";
import { ChangeProformaStatusDialog } from "../../../change-status"; import { ChangeProformaStatusDialog } from "../../../change-status";
import { IssueProformaDialog } from "../../../issue-proforma"; import { IssueProformaDialog } from "../../../issue-proforma";
@ -21,18 +21,20 @@ export const ProformaDetailPage = () => {
if (ctrl.state.pageError) { if (ctrl.state.pageError) {
return ( return (
<div className="p-4 sm:p-6"> <RdxPageFrame>
<div className="mx-auto w-full max-w-3xl"> <RdxPageContainer width="content">
<PageErrorState <RdxPageContent>
description={ctrl.state.pageError.description} <PageErrorState
primaryAction={ctrl.state.pageError.primaryAction} description={ctrl.state.pageError.description}
reference={ctrl.state.pageError.reference} primaryAction={ctrl.state.pageError.primaryAction}
secondaryAction={ctrl.state.pageError.secondaryAction} reference={ctrl.state.pageError.reference}
title={ctrl.state.pageError.title} secondaryAction={ctrl.state.pageError.secondaryAction}
variant={ctrl.state.pageError.variant} title={ctrl.state.pageError.title}
/> variant={ctrl.state.pageError.variant}
</div> />
</div> </RdxPageContent>
</RdxPageContainer>
</RdxPageFrame>
); );
} }

View File

@ -34,6 +34,14 @@ const DetailField = ({ label, value }: DetailFieldProps) => (
</div> </div>
); );
const RecurringProformaDetailSkeleton = ({ label }: { label: string }) => (
<div aria-busy="true" className="space-y-6" role="status">
<div className="h-8 w-64 animate-pulse rounded bg-muted" />
<div className="h-48 animate-pulse rounded-lg bg-muted" />
<span className="sr-only">{label}</span>
</div>
);
export const RecurringProformaDetailPage = () => { export const RecurringProformaDetailPage = () => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@ -48,29 +56,35 @@ export const RecurringProformaDetailPage = () => {
if (feedbackState === "loading") { if (feedbackState === "loading") {
return ( return (
<div aria-busy="true" className="space-y-6 p-4 sm:p-6" role="status"> <RdxPageFrame>
<div className="h-8 w-64 animate-pulse rounded bg-muted" /> <RdxPageContainer width="wide">
<div className="h-48 animate-pulse rounded-lg bg-muted" /> <RdxPageContent>
<span className="sr-only">{t("recurring_proformas.detail.loading")}</span> <RecurringProformaDetailSkeleton label={t("recurring_proformas.detail.loading")} />
</div> </RdxPageContent>
</RdxPageContainer>
</RdxPageFrame>
); );
} }
if (feedbackState === "error") { if (feedbackState === "error") {
return ( return (
<div className="p-4 sm:p-6"> <RdxPageFrame>
<PageErrorState <RdxPageContainer width="content">
description={t("recurring_proformas.detail.error.description")} <RdxPageContent>
primaryAction={{ <PageErrorState
label: t("recurring_proformas.detail.error.retry"), description={t("recurring_proformas.detail.error.description")}
onClick: async () => { primaryAction={{
await query.refetch(); label: t("recurring_proformas.detail.error.retry"),
}, onClick: async () => {
}} await query.refetch();
title={t("recurring_proformas.detail.error.title")} },
variant="unknown" }}
/> title={t("recurring_proformas.detail.error.title")}
</div> variant="unknown"
/>
</RdxPageContent>
</RdxPageContainer>
</RdxPageFrame>
); );
} }

View File

@ -1,6 +1,6 @@
import { PageErrorState } from "@erp/core/components"; import { PageErrorState } from "@erp/core/components";
import { useReturnToNavigation } from "@erp/core/hooks"; import { useReturnToNavigation } from "@erp/core/hooks";
import { RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components"; import { RdxPageContainer, RdxPageContent, RdxPageFrame } from "@repo/rdx-ui/components";
import { Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components"; import { Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
import { import {
Banknote, Banknote,
@ -38,39 +38,41 @@ export const CustomerViewPage = () => {
return ( return (
<RdxPageFrame> <RdxPageFrame>
<RdxPageContainer width="content"> <RdxPageContainer width="content">
<PageErrorState <RdxPageContent>
description={ctrl.pageError.description} <PageErrorState
primaryAction={ description={ctrl.pageError.description}
ctrl.pageError.canRetry primaryAction={
? { ctrl.pageError.canRetry
label: ctrl.isRetrying
? t("view.page_error.actions.retrying")
: t("view.page_error.actions.retry"),
onClick: ctrl.retryView,
isLoading: ctrl.isRetrying,
}
: ctrl.pageError.variant === "forbidden"
? { ? {
label: t("view.page_error.actions.go_home"), label: ctrl.isRetrying
onClick: ctrl.goHome, ? t("view.page_error.actions.retrying")
: t("view.page_error.actions.retry"),
onClick: ctrl.retryView,
isLoading: ctrl.isRetrying,
} }
: { : ctrl.pageError.variant === "forbidden"
? {
label: t("view.page_error.actions.go_home"),
onClick: ctrl.goHome,
}
: {
label: t("view.page_error.actions.back_to_list"),
onClick: ctrl.goBackToList,
}
}
reference={ctrl.pageError.reference}
secondaryAction={
ctrl.pageError.canRetry
? {
label: t("view.page_error.actions.back_to_list"), label: t("view.page_error.actions.back_to_list"),
onClick: ctrl.goBackToList, onClick: ctrl.goBackToList,
} }
} : undefined
reference={ctrl.pageError.reference} }
secondaryAction={ title={ctrl.pageError.title}
ctrl.pageError.canRetry variant={ctrl.pageError.variant}
? { />
label: t("view.page_error.actions.back_to_list"), </RdxPageContent>
onClick: ctrl.goBackToList,
}
: undefined
}
title={ctrl.pageError.title}
variant={ctrl.pageError.variant}
/>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame> </RdxPageFrame>
); );
@ -98,282 +100,288 @@ export const CustomerViewPage = () => {
/> />
} }
> >
<RdxPageContainer className="space-y-6" width="content"> <RdxPageContainer width="content">
{/* Información Básica */} <RdxPageContent>
<Card> {/* Información Básica */}
<CardHeader> <Card>
<CardTitle className="flex items-center gap-2 text-lg"> <CardHeader>
<FileText className="size-5 text-primary" /> <CardTitle className="flex items-center gap-2 text-lg">
{t("view.sections.basic_info")} <FileText className="size-5 text-primary" />
</CardTitle> {t("view.sections.basic_info")}
</CardHeader> </CardTitle>
<CardContent className="space-y-4"> </CardHeader>
<div> <CardContent className="space-y-4">
<dt className="text-sm font-medium text-muted-foreground">{t("view.fields.name")}</dt>
<dd className="mt-1 text-base text-foreground" id="customer-view-name">
{viewCtrl.customerData?.name}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.reference")}
</dt>
<dd className="mt-1 font-mono text-base text-foreground">
{viewCtrl.customerData?.reference}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.legal_record")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.legalRecord}
</dd>
</div>
</CardContent>
</Card>
{/* Dirección */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<MapPin className="size-5 text-primary" />
{t("view.sections.address")}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.street")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.street}
{viewCtrl.customerData?.street2 && (
<>
<br />
{viewCtrl.customerData?.street2}
</>
)}
</dd>
</div>
<div className="grid grid-cols-2 gap-4">
<div> <div>
<dt className="text-sm font-medium text-muted-foreground"> <dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.city")} {t("view.fields.name")}
</dt> </dt>
<dd className="mt-1 text-base text-foreground">{viewCtrl.customerData?.city}</dd> <dd className="mt-1 text-base text-foreground" id="customer-view-name">
</div> {viewCtrl.customerData?.name}
<div>
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.postal_code")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.postalCode}
</dd>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.province")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.province}
</dd> </dd>
</div> </div>
<div> <div>
<dt className="text-sm font-medium text-muted-foreground"> <dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.country")} {t("view.fields.reference")}
</dt> </dt>
<dd className="mt-1 text-base text-foreground">{viewCtrl.customerData?.country}</dd> <dd className="mt-1 font-mono text-base text-foreground">
{viewCtrl.customerData?.reference}
</dd>
</div> </div>
</div> <div>
</CardContent> <dt className="text-sm font-medium text-muted-foreground">
</Card> {t("view.fields.legal_record")}
</dt>
{/* Información de Contacto */} <dd className="mt-1 text-base text-foreground">
<Card className="md:col-span-2"> {viewCtrl.customerData?.legalRecord}
<CardHeader> </dd>
<CardTitle className="flex items-center gap-2 text-lg">
<Mail className="size-5 text-primary" />
{t("view.sections.contact_info")}
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-2">
{/* Contacto Principal */}
<div className="space-y-4">
<h3 className="font-semibold text-foreground">
{t("view.sections.primary_contact")}
</h3>
{viewCtrl.customerData?.primaryEmail && (
<div className="flex items-start gap-3">
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.email")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.primaryEmail}
</dd>
</div>
</div>
)}
{viewCtrl.customerData?.primaryMobile && (
<div className="flex items-start gap-3">
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.mobile")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.primaryMobile}
</dd>
</div>
</div>
)}
{viewCtrl.customerData?.primaryPhone && (
<div className="flex items-start gap-3">
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.phone")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.primaryPhone}
</dd>
</div>
</div>
)}
</div> </div>
</CardContent>
</Card>
{/* Contacto Secundario */} {/* Dirección */}
<div className="space-y-4"> <Card>
<h3 className="font-semibold text-foreground"> <CardHeader>
{t("view.sections.secondary_contact")} <CardTitle className="flex items-center gap-2 text-lg">
</h3> <MapPin className="size-5 text-primary" />
{viewCtrl.customerData?.secondaryEmail && ( {t("view.sections.address")}
<div className="flex items-start gap-3"> </CardTitle>
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" /> </CardHeader>
<div className="flex-1"> <CardContent className="space-y-4">
<dt className="text-sm font-medium text-muted-foreground"> <div>
{t("view.fields.email")} <dt className="text-sm font-medium text-muted-foreground">
</dt> {t("view.fields.street")}
<dd className="mt-1 text-base text-foreground"> </dt>
{viewCtrl.customerData?.secondaryEmail} <dd className="mt-1 text-base text-foreground">
</dd> {viewCtrl.customerData?.street}
</div> {viewCtrl.customerData?.street2 && (
</div> <>
)} <br />
{viewCtrl.customerData?.secondaryMobile && ( {viewCtrl.customerData?.street2}
<div className="flex items-start gap-3"> </>
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" /> )}
<div className="flex-1"> </dd>
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.mobile")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.secondaryMobile}
</dd>
</div>
</div>
)}
{viewCtrl.customerData?.secondaryPhone && (
<div className="flex items-start gap-3">
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.phone")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.secondaryPhone}
</dd>
</div>
</div>
)}
</div> </div>
<div className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.city")}
</dt>
<dd className="mt-1 text-base text-foreground">{viewCtrl.customerData?.city}</dd>
</div>
<div>
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.postal_code")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.postalCode}
</dd>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.province")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.province}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.country")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.country}
</dd>
</div>
</div>
</CardContent>
</Card>
{/* Otros Contactos */} {/* Información de Contacto */}
{(viewCtrl.customerData?.website || viewCtrl.customerData?.fax) && ( <Card className="md:col-span-2">
<div className="space-y-4 md:col-span-2"> <CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Mail className="size-5 text-primary" />
{t("view.sections.contact_info")}
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-2">
{/* Contacto Principal */}
<div className="space-y-4">
<h3 className="font-semibold text-foreground"> <h3 className="font-semibold text-foreground">
{t("view.sections.other_contacts")} {t("view.sections.primary_contact")}
</h3> </h3>
<div className="grid gap-4 md:grid-cols-2"> {viewCtrl.customerData?.primaryEmail && (
{viewCtrl.customerData?.website && ( <div className="flex items-start gap-3">
<div className="flex items-start gap-3"> <Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
<Globe className="mt-0.5 h-4 w-4 text-muted-foreground" /> <div className="flex-1">
<div className="flex-1"> <dt className="text-sm font-medium text-muted-foreground">
<dt className="text-sm font-medium text-muted-foreground"> {t("view.fields.email")}
{t("view.fields.website")} </dt>
</dt> <dd className="mt-1 text-base text-foreground">
<dd className="mt-1 text-base text-primary hover:underline"> {viewCtrl.customerData?.primaryEmail}
<a </dd>
href={viewCtrl.customerData?.website}
rel="noopener noreferrer"
target="_blank"
>
{viewCtrl.customerData?.website}
</a>
</dd>
</div>
</div> </div>
)} </div>
{viewCtrl.customerData?.fax && ( )}
<div className="flex items-start gap-3"> {viewCtrl.customerData?.primaryMobile && (
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" /> <div className="flex items-start gap-3">
<div className="flex-1"> <Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
<dt className="text-sm font-medium text-muted-foreground"> <div className="flex-1">
{t("view.fields.fax")} <dt className="text-sm font-medium text-muted-foreground">
</dt> {t("view.fields.mobile")}
<dd className="mt-1 text-base text-foreground"> </dt>
{viewCtrl.customerData?.fax} <dd className="mt-1 text-base text-foreground">
</dd> {viewCtrl.customerData?.primaryMobile}
</div> </dd>
</div> </div>
)} </div>
)}
{viewCtrl.customerData?.primaryPhone && (
<div className="flex items-start gap-3">
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.phone")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.primaryPhone}
</dd>
</div>
</div>
)}
</div>
{/* Contacto Secundario */}
<div className="space-y-4">
<h3 className="font-semibold text-foreground">
{t("view.sections.secondary_contact")}
</h3>
{viewCtrl.customerData?.secondaryEmail && (
<div className="flex items-start gap-3">
<Mail className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.email")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.secondaryEmail}
</dd>
</div>
</div>
)}
{viewCtrl.customerData?.secondaryMobile && (
<div className="flex items-start gap-3">
<Smartphone className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.mobile")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.secondaryMobile}
</dd>
</div>
</div>
)}
{viewCtrl.customerData?.secondaryPhone && (
<div className="flex items-start gap-3">
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.phone")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.secondaryPhone}
</dd>
</div>
</div>
)}
</div>
{/* Otros Contactos */}
{(viewCtrl.customerData?.website || viewCtrl.customerData?.fax) && (
<div className="space-y-4 md:col-span-2">
<h3 className="font-semibold text-foreground">
{t("view.sections.other_contacts")}
</h3>
<div className="grid gap-4 md:grid-cols-2">
{viewCtrl.customerData?.website && (
<div className="flex items-start gap-3">
<Globe className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.website")}
</dt>
<dd className="mt-1 text-base text-primary hover:underline">
<a
href={viewCtrl.customerData?.website}
rel="noopener noreferrer"
target="_blank"
>
{viewCtrl.customerData?.website}
</a>
</dd>
</div>
</div>
)}
{viewCtrl.customerData?.fax && (
<div className="flex items-start gap-3">
<Phone className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.fax")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.fax}
</dd>
</div>
</div>
)}
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* Preferencias */}
<Card className="md:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Languages className="size-5 text-primary" />
{t("view.sections.preferences")}
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-2">
<div className="flex items-start gap-3">
<Languages className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.language_code")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.languageCode}
</dd>
</div> </div>
</div> </div>
)} <div className="flex items-start gap-3">
</div> <Banknote className="mt-0.5 h-4 w-4 text-muted-foreground" />
</CardContent> <div className="flex-1">
</Card> <dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.currency_code")}
{/* Preferencias */} </dt>
<Card className="md:col-span-2"> <dd className="mt-1 text-base text-foreground">
<CardHeader> {viewCtrl.customerData?.currencyCode}
<CardTitle className="flex items-center gap-2 text-lg"> </dd>
<Languages className="size-5 text-primary" /> </div>
{t("view.sections.preferences")}
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-2">
<div className="flex items-start gap-3">
<Languages className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="flex-1">
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.language_code")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.languageCode}
</dd>
</div> </div>
</div> </div>
<div className="flex items-start gap-3"> </CardContent>
<Banknote className="mt-0.5 h-4 w-4 text-muted-foreground" /> </Card>
<div className="flex-1"> </RdxPageContent>
<dt className="text-sm font-medium text-muted-foreground">
{t("view.fields.currency_code")}
</dt>
<dd className="mt-1 text-base text-foreground">
{viewCtrl.customerData?.currencyCode}
</dd>
</div>
</div>
</div>
</CardContent>
</Card>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame> </RdxPageFrame>
); );

View File

@ -6,7 +6,7 @@ const SuppliersListPage = lazy(() =>
import("./list").then((module) => ({ default: module.SuppliersListPage })) import("./list").then((module) => ({ default: module.SuppliersListPage }))
); );
const SupplierDetailPage = lazy(() => const SupplierDetailPage = lazy(() =>
import("./view").then((module) => ({ default: module.SupplierDetailPage })) import("./view").then((module) => ({ default: module.SupplierViewPage }))
); );
const SupplierCreatePage = lazy(() => const SupplierCreatePage = lazy(() =>
import("./create").then((module) => ({ default: module.SupplierCreatePage })) import("./create").then((module) => ({ default: module.SupplierCreatePage }))

View File

@ -1 +1 @@
export * from "./supplier-detail-page"; export * from "./supplier-view-page";

View File

@ -23,8 +23,13 @@ const DetailField = ({ label, value }: { label: ReactNode; value?: ReactNode })
</div> </div>
); );
export const SupplierDetailPage = () => { const SupplierViewContent = ({ children }: { children: ReactNode }) => (
<div className="grid gap-4 lg:grid-cols-2">{children}</div>
);
export const SupplierViewPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { id } = useParams(); const { id } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -40,18 +45,20 @@ export const SupplierDetailPage = () => {
return ( return (
<RdxPageFrame> <RdxPageFrame>
<RdxPageContainer width="content"> <RdxPageContainer width="content">
<PageErrorState <RdxPageContent>
description={t("detail.error.description")} <PageErrorState
primaryAction={{ description={t("detail.error.description")}
label: t("common.actions.retry"), primaryAction={{
onClick: async () => { label: t("common.actions.retry"),
await query.refetch(); onClick: async () => {
}, await query.refetch();
}} },
secondaryAction={{ label: t("common.actions.back"), onClick: navigateBack }} }}
title={t("detail.error.title")} secondaryAction={{ label: t("common.actions.back"), onClick: navigateBack }}
variant="notFound" title={t("detail.error.title")}
/> variant="notFound"
/>
</RdxPageContent>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame> </RdxPageFrame>
); );
@ -100,7 +107,7 @@ export const SupplierDetailPage = () => {
> >
<RdxPageContainer width="full"> <RdxPageContainer width="full">
<RdxPageContent spacing="default"> <RdxPageContent spacing="default">
<div className="grid gap-4 lg:grid-cols-2"> <SupplierViewContent>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="flex items-center gap-2 text-lg">
@ -185,7 +192,7 @@ export const SupplierDetailPage = () => {
</dl> </dl>
</CardContent> </CardContent>
</Card> </Card>
</div> </SupplierViewContent>
</RdxPageContent> </RdxPageContent>
</RdxPageContainer> </RdxPageContainer>
</RdxPageFrame> </RdxPageFrame>