Compare commits
4 Commits
a13ababdf6
...
6b05ee01f3
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b05ee01f3 | |||
| ed58bd1e64 | |||
| c6de77897d | |||
| 9f303819b3 |
@ -8,7 +8,7 @@ interface AppFullscreenLayoutFrameProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AppFullscreenLayoutFrame = ({ children }: AppFullscreenLayoutFrameProps) => {
|
export const AppFullscreenLayoutFrame = ({ children }: AppFullscreenLayoutFrameProps) => {
|
||||||
return <AppMain className="h-dvh w-dvw">{children}</AppMain>;
|
return <AppMain className="h-dvh w-dvw rdx-app-fullscreem-layout">{children}</AppMain>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const AppSidebarLayoutFrame = ({ children }: AppSidebarLayoutFrameProps)
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<SidebarInset className="flex h-dvh min-w-0 flex-col overflow-hidden bg-background">
|
<SidebarInset className="flex h-dvh min-w-0 flex-col overflow-hidden bg-background rdx-app-sidebar-layout">
|
||||||
<AppTopbar />
|
<AppTopbar />
|
||||||
<AppMain>{children}</AppMain>
|
<AppMain>{children}</AppMain>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
|
|||||||
@ -68,9 +68,22 @@ AppMain
|
|||||||
├── content
|
├── content
|
||||||
│ └── RdxPageContainer
|
│ └── RdxPageContainer
|
||||||
│ └── RdxPageContent
|
│ └── RdxPageContent
|
||||||
└── footer opcional
|
└── footer opcional
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
RdxPageFrame
|
||||||
|
→ estructura y scroll
|
||||||
|
|
||||||
|
RdxPageContainer
|
||||||
|
→ ancho y padding
|
||||||
|
|
||||||
|
RdxPageContent
|
||||||
|
→ composición de los hijos principales
|
||||||
|
```
|
||||||
|
|
||||||
|
Regla: Las Pages no deben reconstruir estas responsabilidades con wrappers HTML locales.
|
||||||
|
|
||||||
## 6. Identity y sesión
|
## 6. Identity y sesión
|
||||||
|
|
||||||
El runtime actual se compone en `apps/web/src/app.tsx`:
|
El runtime actual se compone en `apps/web/src/app.tsx`:
|
||||||
|
|||||||
@ -36,18 +36,20 @@ El controller de página concentra URL, filtros, consulta, estados y navegación
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<RdxPageFrame
|
<RdxPageFrame
|
||||||
header={<RdxEditorHeader title={title} onBack={onBack} />}
|
header={<RdxEditorHeader />}
|
||||||
footer={<RdxEditorActionBar secondary={cancel} primary={submit} />}
|
footer={<RdxEditorActionBar />}
|
||||||
>
|
>
|
||||||
<RdxPageContainer width="content">
|
<RdxPageContainer width="content">
|
||||||
<RdxPageContent aside={aside}>
|
<RdxPageContent>
|
||||||
{notice}
|
{pageError ? <ErrorAlert /> : null}
|
||||||
<RdxPanel title={sectionTitle}>{fields}</RdxPanel>
|
<Editor />
|
||||||
</RdxPageContent>
|
</RdxPageContent>
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
</RdxPageFrame>
|
</RdxPageFrame>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Los errores inline no definen su propio spacing estructural. Participan del spacing estándar de RdxPageContent igual que el resto de hijos principales.
|
||||||
|
|
||||||
Footer y aside son opcionales. `content` es adecuado para formularios focalizados; `full` para editores documentales amplios. Create y Update deben compartir geometría cuando solo cambian defaults, submit o navegación.
|
Footer y aside son opcionales. `content` es adecuado para formularios focalizados; `full` para editores documentales amplios. Create y Update deben compartir geometría cuando solo cambian defaults, submit o navegación.
|
||||||
|
|
||||||
Loading y error bloqueante usan también `RdxPageFrame` y `RdxPageContainer`; no reconstruyen el viewport.
|
Loading y error bloqueante usan también `RdxPageFrame` y `RdxPageContainer`; no reconstruyen el viewport.
|
||||||
|
|||||||
@ -144,6 +144,21 @@ Excepción: un detalle documental puede configurar el contenido del frame con `o
|
|||||||
- Tablas y editores de líneas conservan su layout funcional.
|
- Tablas y editores de líneas conservan su layout funcional.
|
||||||
- Evitar card dentro de card y sombras acumuladas.
|
- Evitar card dentro de card y sombras acumuladas.
|
||||||
|
|
||||||
|
### Composición del contenido principal
|
||||||
|
|
||||||
|
`RdxPageContent` es el compositor estructural de los hijos principales de una página.
|
||||||
|
|
||||||
|
Los avisos, errores inline, secciones y contenido funcional deben ser hijos directos cuando pertenezcan al mismo nivel visual.
|
||||||
|
`RdxPanel` no es un wrapper genérico. Se usa únicamente cuando existe una sección con identidad visual o semántica propia.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<RdxPageContent>
|
||||||
|
{error ? <ErrorAlert /> : null}
|
||||||
|
<Editor />
|
||||||
|
</RdxPageContent>
|
||||||
|
```
|
||||||
|
No deben introducirse wrappers adicionales únicamente para controlar gap, ancho, padding o centrado.
|
||||||
|
|
||||||
## 8. Responsive y accesibilidad
|
## 8. Responsive y accesibilidad
|
||||||
|
|
||||||
- Las columnas se apilan antes de producir overflow horizontal.
|
- Las columnas se apilan antes de producir overflow horizontal.
|
||||||
|
|||||||
@ -12,7 +12,10 @@ export const ProformaDetailHeaderMeta = ({
|
|||||||
const items = useMemo(() => [customerName, documentReference], [customerName, documentReference]);
|
const items = useMemo(() => [customerName, documentReference], [customerName, documentReference]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-sm font-medium text-muted-foreground">
|
<div
|
||||||
|
className="flex flex-wrap items-center gap-x-3 gap-y-2 text-sm font-medium text-muted-foreground"
|
||||||
|
id="proforma-detail-identity"
|
||||||
|
>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<div className="flex items-center gap-3" key={`${item}-${index}`}>
|
<div className="flex items-center gap-3" key={`${item}-${index}`}>
|
||||||
{index > 0 ? <span className="hidden text-border sm:inline">|</span> : null}
|
{index > 0 ? <span className="hidden text-border sm:inline">|</span> : null}
|
||||||
|
|||||||
@ -246,6 +246,7 @@ export function ProformaRowActions({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RdxRowActions
|
<RdxRowActions
|
||||||
|
data-testid={`proforma-row-${proforma.id}`}
|
||||||
overflowActions={menuActions}
|
overflowActions={menuActions}
|
||||||
overflowLabel={t("common.more_actions")}
|
overflowLabel={t("common.more_actions")}
|
||||||
primaryActions={visibleActions}
|
primaryActions={visibleActions}
|
||||||
|
|||||||
@ -72,7 +72,12 @@ export const SelectedRecipientSummary = ({
|
|||||||
<div className="min-w-0 space-y-3">
|
<div className="min-w-0 space-y-3">
|
||||||
<div className="min-w-0 space-y-1">
|
<div className="min-w-0 space-y-1">
|
||||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<p className="min-w-0 wrap-break-word text-base font-semibold">{recipient.name}</p>
|
<p
|
||||||
|
className="min-w-0 wrap-break-word text-base font-semibold"
|
||||||
|
id="proforma-recipient-name"
|
||||||
|
>
|
||||||
|
{recipient.name}
|
||||||
|
</p>
|
||||||
|
|
||||||
{recipient.status ? (
|
{recipient.status ? (
|
||||||
<RdxBadge
|
<RdxBadge
|
||||||
|
|||||||
@ -49,14 +49,22 @@ export const ProformaUpdatePage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (ctrl.isInitialLoading) {
|
if (ctrl.isInitialLoading) {
|
||||||
return <ProformaUpdateSkeleton />;
|
return (
|
||||||
|
<RdxPageFrame>
|
||||||
|
<RdxPageContainer width="content">
|
||||||
|
<RdxPageContent>
|
||||||
|
<ProformaUpdateSkeleton />
|
||||||
|
</RdxPageContent>
|
||||||
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctrl.pageError) {
|
if (ctrl.pageError) {
|
||||||
return (
|
return (
|
||||||
<RdxPageFrame>
|
<RdxPageFrame>
|
||||||
<RdxPageContainer width="full">
|
<RdxPageContainer width="content">
|
||||||
<div className="mx-auto w-full max-w-3xl">
|
<RdxPageContent>
|
||||||
<PageErrorState
|
<PageErrorState
|
||||||
description={ctrl.pageError.description}
|
description={ctrl.pageError.description}
|
||||||
primaryAction={
|
primaryAction={
|
||||||
@ -90,13 +98,12 @@ export const ProformaUpdatePage = () => {
|
|||||||
title={ctrl.pageError.title}
|
title={ctrl.pageError.title}
|
||||||
variant={ctrl.pageError.variant}
|
variant={ctrl.pageError.variant}
|
||||||
/>
|
/>
|
||||||
</div>
|
</RdxPageContent>
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
</RdxPageFrame>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasPageAlerts = updateCtrl.isUpdateError || updateCtrl.isSeriesLoadError;
|
|
||||||
const actionBar = (
|
const actionBar = (
|
||||||
<RdxEditorActionBar
|
<RdxEditorActionBar
|
||||||
leading={
|
leading={
|
||||||
@ -120,6 +127,7 @@ export const ProformaUpdatePage = () => {
|
|||||||
primary={
|
primary={
|
||||||
<RhfSubmitActionButton
|
<RhfSubmitActionButton
|
||||||
busyLabel="Guardando..."
|
busyLabel="Guardando..."
|
||||||
|
data-testid="proforma-update-submit"
|
||||||
disabled={updateCtrl.isUpdating}
|
disabled={updateCtrl.isUpdating}
|
||||||
formId={updateCtrl.formId}
|
formId={updateCtrl.formId}
|
||||||
isBusy={updateCtrl.form.formState.isSubmitting}
|
isBusy={updateCtrl.form.formState.isSubmitting}
|
||||||
@ -188,61 +196,50 @@ export const ProformaUpdatePage = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<RdxPageContainer width="full">
|
<RdxPageContainer width="full">
|
||||||
{hasPageAlerts ? (
|
<RdxPageContent>
|
||||||
<RdxPageContainer className="pb-0" width="full">
|
{updateCtrl.isUpdateError ? (
|
||||||
<RdxPageContent spacing="compact">
|
<ErrorAlert
|
||||||
{updateCtrl.isUpdateError && (
|
message={
|
||||||
<ErrorAlert
|
(updateCtrl.updateError as Error)?.message ??
|
||||||
message={
|
t("pages.proformas.update.error_msg", "Revisa los datos e inténtalo de nuevo.")
|
||||||
(updateCtrl.updateError as Error)?.message ??
|
}
|
||||||
t(
|
title={t("pages.proformas.update.error_title", "No se pudo guardar los cambios")}
|
||||||
"pages.proformas.update.error_msg",
|
/>
|
||||||
"Revisa los datos e inténtalo de nuevo."
|
) : null}
|
||||||
|
|
||||||
|
{updateCtrl.isSeriesLoadError ? (
|
||||||
|
<ErrorAlert
|
||||||
|
message={
|
||||||
|
updateCtrl.seriesLoadError instanceof Error
|
||||||
|
? updateCtrl.seriesLoadError.message
|
||||||
|
: t(
|
||||||
|
"pages.proformas.update.series_load_error.message",
|
||||||
|
"Inténtalo de nuevo más tarde."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
title={t(
|
title={t(
|
||||||
"pages.proformas.update.error_title",
|
"pages.proformas.update.series_load_error.title",
|
||||||
"No se pudo guardar los cambios"
|
"No se pudieron cargar las series de facturación"
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{updateCtrl.isSeriesLoadError && (
|
<ProformaUpdateEditorForm
|
||||||
<ErrorAlert
|
formId={updateCtrl.formId}
|
||||||
message={
|
isSubmitting={updateCtrl.isUpdating}
|
||||||
updateCtrl.seriesLoadError instanceof Error
|
itemsCtrl={updateCtrl.itemsCtrl}
|
||||||
? updateCtrl.seriesLoadError.message
|
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
||||||
: t(
|
onCreateCustomerClick={selectCustomerCtrl.openQuickCreateDialog}
|
||||||
"pages.proformas.update.series_load_error.message",
|
onPaymentTermChange={updateCtrl.handlePaymentTermChange}
|
||||||
"Inténtalo de nuevo más tarde."
|
onReset={updateCtrl.resetForm}
|
||||||
)
|
onSubmit={updateCtrl.onSubmit}
|
||||||
}
|
paymentCtrl={updateCtrl.paymentCtrl}
|
||||||
title={t(
|
selectedCustomer={updateCtrl.selectedCustomer}
|
||||||
"pages.proformas.update.series_load_error.title",
|
seriesOptions={updateCtrl.seriesOptions}
|
||||||
"No se pudieron cargar las series de facturación"
|
taxCtrl={updateCtrl.taxCtrl}
|
||||||
)}
|
totalsCtrl={updateCtrl.totalsCtrl}
|
||||||
/>
|
/>
|
||||||
)}
|
</RdxPageContent>
|
||||||
</RdxPageContent>
|
|
||||||
</RdxPageContainer>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<ProformaUpdateEditorForm
|
|
||||||
className={hasPageAlerts ? "pt-0" : undefined}
|
|
||||||
formId={updateCtrl.formId}
|
|
||||||
isSubmitting={updateCtrl.isUpdating}
|
|
||||||
itemsCtrl={updateCtrl.itemsCtrl}
|
|
||||||
onChangeCustomerClick={selectCustomerCtrl.selectCtrl.openDialog}
|
|
||||||
onCreateCustomerClick={selectCustomerCtrl.openQuickCreateDialog}
|
|
||||||
onPaymentTermChange={updateCtrl.handlePaymentTermChange}
|
|
||||||
onReset={updateCtrl.resetForm}
|
|
||||||
onSubmit={updateCtrl.onSubmit}
|
|
||||||
paymentCtrl={updateCtrl.paymentCtrl}
|
|
||||||
selectedCustomer={updateCtrl.selectedCustomer}
|
|
||||||
seriesOptions={updateCtrl.seriesOptions}
|
|
||||||
taxCtrl={updateCtrl.taxCtrl}
|
|
||||||
totalsCtrl={updateCtrl.totalsCtrl}
|
|
||||||
/>
|
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
</RdxPageFrame>
|
</RdxPageFrame>
|
||||||
|
|
||||||
|
|||||||
@ -88,7 +88,11 @@ export const SelectCustomerDialog = ({
|
|||||||
<Button onClick={ctrl.closeDialog} variant="outline">
|
<Button onClick={ctrl.closeDialog} variant="outline">
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={ctrl.isLoading || !selectedCustomer} onClick={handleConfirm}>
|
<Button
|
||||||
|
disabled={ctrl.isLoading || !selectedCustomer}
|
||||||
|
id="customer-selection-confirm"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
>
|
||||||
{selectedCustomer ? "Confirmar selección" : "Seleccionar cliente"}
|
{selectedCustomer ? "Confirmar selección" : "Seleccionar cliente"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export const SelectCustomerSelectionList = ({
|
|||||||
"cursor-pointer",
|
"cursor-pointer",
|
||||||
customer.id === selectedCustomer?.id && "border-primary bg-accent"
|
customer.id === selectedCustomer?.id && "border-primary bg-accent"
|
||||||
)}
|
)}
|
||||||
|
data-testid={`customer-selection-${customer.id}`}
|
||||||
key={customer.id}
|
key={customer.id}
|
||||||
onClick={() => onSelect(customer)}
|
onClick={() => onSelect(customer)}
|
||||||
role="listitem"
|
role="listitem"
|
||||||
|
|||||||
@ -29,52 +29,51 @@ export const CustomerUpdatePage = () => {
|
|||||||
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("update.page_error.actions.retrying")
|
|
||||||
: t("update.page_error.actions.retry"),
|
|
||||||
onClick: ctrl.retryUpdate,
|
|
||||||
isLoading: ctrl.isRetrying,
|
|
||||||
}
|
|
||||||
: ctrl.pageError.variant === "forbidden"
|
|
||||||
? {
|
? {
|
||||||
label: t("update.page_error.actions.go_home"),
|
label: ctrl.isRetrying
|
||||||
onClick: ctrl.goHome,
|
? t("update.page_error.actions.retrying")
|
||||||
|
: t("update.page_error.actions.retry"),
|
||||||
|
onClick: ctrl.retryUpdate,
|
||||||
|
isLoading: ctrl.isRetrying,
|
||||||
}
|
}
|
||||||
: {
|
: ctrl.pageError.variant === "forbidden"
|
||||||
|
? {
|
||||||
|
label: t("update.page_error.actions.go_home"),
|
||||||
|
onClick: ctrl.goHome,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
label: t("update.page_error.actions.back_to_list"),
|
||||||
|
onClick: ctrl.goBackToList,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reference={ctrl.pageError.reference}
|
||||||
|
secondaryAction={
|
||||||
|
ctrl.pageError.canRetry
|
||||||
|
? {
|
||||||
label: t("update.page_error.actions.back_to_list"),
|
label: t("update.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("update.page_error.actions.back_to_list"),
|
</RdxPageContent>
|
||||||
onClick: ctrl.goBackToList,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
title={ctrl.pageError.title}
|
|
||||||
variant={ctrl.pageError.variant}
|
|
||||||
/>
|
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
</RdxPageFrame>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasPageAlerts = updateCtrl.isUpdateError;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...updateCtrl.form}>
|
<FormProvider {...updateCtrl.form}>
|
||||||
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
||||||
<RdxPageFrame
|
<RdxPageFrame
|
||||||
contentClassName="bg-muted/20"
|
contentClassName="bg-red-500"
|
||||||
footer={<h1>hola</h1>}
|
|
||||||
header={
|
header={
|
||||||
<CustomerUpdateHeader
|
<CustomerUpdateHeader
|
||||||
formId={updateCtrl.formId}
|
formId={updateCtrl.formId}
|
||||||
@ -99,28 +98,16 @@ export const CustomerUpdatePage = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<RdxPageContainer className="bg-white" width="content">
|
<RdxPageContainer width="content">
|
||||||
<RdxPageContent>
|
<RdxPageContent>
|
||||||
{hasPageAlerts ? (
|
{updateCtrl.isUpdateError ? (
|
||||||
<RdxPageContainer className="pb-0" width="full">
|
<ErrorAlert
|
||||||
<div className="flex min-w-0 flex-col gap-4">
|
message={
|
||||||
{updateCtrl.isUpdateError && (
|
(updateCtrl.updateError as Error)?.message ??
|
||||||
<ErrorAlert
|
t("pages.customers.update.error_msg", "Revisa los datos e inténtalo de nuevo.")
|
||||||
message={
|
}
|
||||||
(updateCtrl.updateError as Error)?.message ??
|
title={t("pages.customers.update.error_title", "No se pudo guardar los cambios")}
|
||||||
t(
|
/>
|
||||||
"pages.customers.update.error_msg",
|
|
||||||
"Revisa los datos e inténtalo de nuevo."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
title={t(
|
|
||||||
"pages.customers.update.error_title",
|
|
||||||
"No se pudo guardar los cambios"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</RdxPageContainer>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<CustomerUpdateEditorForm
|
<CustomerUpdateEditorForm
|
||||||
@ -130,26 +117,6 @@ export const CustomerUpdatePage = () => {
|
|||||||
onReset={updateCtrl.resetForm}
|
onReset={updateCtrl.resetForm}
|
||||||
onSubmit={updateCtrl.onSubmit}
|
onSubmit={updateCtrl.onSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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">
|
|
||||||
{updateCtrl.isUpdateError ? (
|
|
||||||
<ErrorAlert
|
|
||||||
message={
|
|
||||||
(updateCtrl.updateError as Error)?.message ??
|
|
||||||
t(
|
|
||||||
"pages.customers.update.error_msg",
|
|
||||||
"Revisa los datos e inténtalo de nuevo."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
title={t(
|
|
||||||
"pages.customers.update.error_title",
|
|
||||||
"No se pudo guardar los cambios"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</RdxPageContent>
|
</RdxPageContent>
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
</RdxPageFrame>
|
</RdxPageFrame>
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { loginViaUi } from "../../support/login-via-ui";
|
||||||
|
|
||||||
|
import { createProformaViaUi } from "./support/create-proforma-via-ui";
|
||||||
|
|
||||||
|
test("CUSTOMER-INVOICES-PROFORMA-CREATE-001: crea un borrador mínimo sin líneas y comprueba persistencia", async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
await loginViaUi(page);
|
||||||
|
const proforma = await createProformaViaUi(page, testInfo);
|
||||||
|
await page.reload();
|
||||||
|
await expect(page).toHaveURL((url) => url.pathname === `/proformas/${proforma.id}/edit`);
|
||||||
|
await expect(page.locator('input[name="proformaReference"]')).toHaveValue(proforma.reference);
|
||||||
|
await expect(page.locator("#proforma-recipient-name")).toHaveText(proforma.customer.name);
|
||||||
|
});
|
||||||
21
tests/e2e/customer-invoices/proformas/proforma-list.spec.ts
Normal file
21
tests/e2e/customer-invoices/proformas/proforma-list.spec.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { loginViaUi } from "../../support/login-via-ui";
|
||||||
|
|
||||||
|
import { createProformaViaUi } from "./support/create-proforma-via-ui";
|
||||||
|
|
||||||
|
test("CUSTOMER-INVOICES-PROFORMA-LIST-001: muestra la proforma persistida propia del escenario", async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
await loginViaUi(page);
|
||||||
|
const proforma = await createProformaViaUi(page, testInfo);
|
||||||
|
await page.goto("/proformas");
|
||||||
|
// La referencia asignada por el servidor permite aislar el registro sin depender de su página.
|
||||||
|
await page.locator('input[inputmode="search"]').fill(proforma.reference);
|
||||||
|
const row = page
|
||||||
|
.getByRole("row")
|
||||||
|
.filter({ has: page.getByTestId(`proforma-row-${proforma.id}`) });
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await expect(row).toContainText(proforma.reference);
|
||||||
|
await expect(row).toContainText(proforma.customer.name);
|
||||||
|
});
|
||||||
19
tests/e2e/customer-invoices/proformas/proforma-view.spec.ts
Normal file
19
tests/e2e/customer-invoices/proformas/proforma-view.spec.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { loginViaUi } from "../../support/login-via-ui";
|
||||||
|
|
||||||
|
import { createProformaViaUi } from "./support/create-proforma-via-ui";
|
||||||
|
|
||||||
|
test("CUSTOMER-INVOICES-PROFORMA-VIEW-001: muestra referencia y cliente persistidos al abrir y recargar detalle", async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
await loginViaUi(page);
|
||||||
|
const proforma = await createProformaViaUi(page, testInfo);
|
||||||
|
await page.goto(`/proformas/${proforma.id}`);
|
||||||
|
const identity = page.locator("#proforma-detail-identity");
|
||||||
|
await expect(identity).toContainText(proforma.reference);
|
||||||
|
await expect(identity).toContainText(proforma.customer.name);
|
||||||
|
await page.reload();
|
||||||
|
await expect(identity).toContainText(proforma.reference);
|
||||||
|
await expect(identity).toContainText(proforma.customer.name);
|
||||||
|
});
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
|
import type { Page, TestInfo } from "@playwright/test";
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
|
import { createCustomerViaUi } from "../../../customers/support/create-customer-via-ui";
|
||||||
|
|
||||||
|
export async function createProformaViaUi(page: Page, testInfo: TestInfo) {
|
||||||
|
const token = `e2eproforma${randomUUID().replaceAll("-", "")}`;
|
||||||
|
const customer = await createCustomerViaUi(page, testInfo, `E2E Proforma ${token}`);
|
||||||
|
await page.goto("/proformas/create");
|
||||||
|
await page.locator("#proforma-create-customer").click();
|
||||||
|
await page.getByRole("dialog").locator('input[inputmode="search"]').fill(token);
|
||||||
|
await page.getByTestId(`customer-selection-${customer.id}`).click();
|
||||||
|
await page.locator("#customer-selection-confirm").click();
|
||||||
|
await expect(page.locator("#proforma-create-customer")).toContainText(customer.name);
|
||||||
|
await page.getByTestId("submit-button").click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/proformas\/[^/]+\/edit\?returnTo=/);
|
||||||
|
const referenceInput = page.locator('input[name="proformaReference"]');
|
||||||
|
await expect(referenceInput).toHaveValue(/\S+/);
|
||||||
|
await expect(page.locator("#proforma-recipient-name")).toHaveText(customer.name);
|
||||||
|
const reference = await referenceInput.inputValue();
|
||||||
|
const id = new URL(page.url()).pathname.split("/")[2];
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("Missing proforma ID in edit URL");
|
||||||
|
}
|
||||||
|
return { id, reference, customer, token };
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { loginViaUi } from "../../support/login-via-ui";
|
||||||
|
|
||||||
|
import { createProformaViaUi } from "./support/create-proforma-via-ui";
|
||||||
|
|
||||||
|
test("CUSTOMER-INVOICES-PROFORMA-UPDATE-001: modifica referencia y descripción del borrador y comprueba persistencia", async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
await loginViaUi(page);
|
||||||
|
const proforma = await createProformaViaUi(page, testInfo);
|
||||||
|
const reference = `E2E ${proforma.token}`;
|
||||||
|
const description = `E2E updated draft ${proforma.token}`;
|
||||||
|
await page.locator('input[name="reference"]').fill(reference);
|
||||||
|
await page.locator('input[name="description"]').fill(description);
|
||||||
|
const responsePromise = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.request().method() === "PUT" &&
|
||||||
|
new URL(response.url()).pathname.endsWith(`/proformas/${proforma.id}`)
|
||||||
|
);
|
||||||
|
await page.getByTestId("proforma-update-submit").click();
|
||||||
|
expect((await responsePromise).status()).toBe(200);
|
||||||
|
await expect(page).toHaveURL((url) => url.pathname === `/proformas/${proforma.id}/edit`);
|
||||||
|
await expect(page.locator('input[name="reference"]')).toHaveValue(reference);
|
||||||
|
await expect(page.locator('input[name="description"]')).toHaveValue(description);
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.locator('input[name="reference"]')).toHaveValue(reference);
|
||||||
|
await expect(page.locator('input[name="description"]')).toHaveValue(description);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user