-
-
-
- Customer selector and helper actions
-
-
+
+
+
+
+ Customer selector and helper actions
+
+
-
-
-
- Tax regime field
-
-
- Default tax selector
-
-
-
-
- }
- complexity="complex"
- >
-
-
-
- Inline feedback aligned with the editor content
+
+
+
+ Tax regime field
+
+
+ Default tax selector
+
+
+
-
-
-
-
- Reference field
-
-
- Date field
-
-
- Series field
-
-
-
-
-
-
- Line editor keeps its own internal table layout and stays in the
- main surface.
+ }
+ >
+
+
+
+ Inline feedback aligned with the editor content
-
+
+
+
+
+ Reference field
+
+
+ Date field
+
+
+ Series field
+
+
+
+
+
+
+ Line editor keeps its own internal table layout and stays in the
+ main surface.
+
+
+
-
-
+
+
{
+ const html = renderToStaticMarkup();
+
+ assert.match(html, /flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden/);
+ assert.match(html, /rdx-page-container/);
+ assert.doesNotMatch(html, /fixed inset-0/);
+ assert.equal(html.match(/overflow-y-auto/g)?.length, 1);
+});
diff --git a/modules/customer-invoices/src/web/proformas/create/ui/components/proforma-create-skeleton.tsx b/modules/customer-invoices/src/web/proformas/create/ui/components/proforma-create-skeleton.tsx
index dc8d2e3e..cfb752ea 100644
--- a/modules/customer-invoices/src/web/proformas/create/ui/components/proforma-create-skeleton.tsx
+++ b/modules/customer-invoices/src/web/proformas/create/ui/components/proforma-create-skeleton.tsx
@@ -1,16 +1,25 @@
+import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
import { Skeleton } from "@repo/shadcn-ui/components";
export const ProformaCreateSkeleton = () => {
return (
-
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/modules/customer-invoices/src/web/proformas/detail/controllers/use-create-recurring-proforma-from-proforma-dialog-controller.ts b/modules/customer-invoices/src/web/proformas/detail/controllers/use-create-recurring-proforma-from-proforma-dialog-controller.ts
index 7b0da479..965c2896 100644
--- a/modules/customer-invoices/src/web/proformas/detail/controllers/use-create-recurring-proforma-from-proforma-dialog-controller.ts
+++ b/modules/customer-invoices/src/web/proformas/detail/controllers/use-create-recurring-proforma-from-proforma-dialog-controller.ts
@@ -18,10 +18,10 @@ import { useCreateRecurringProformaFromProforma } from "../../../recurring-profo
import { buildRecurringProformaDetailPath } from "../../../recurring-proformas/shared/recurring-proforma-navigation";
import {
- buildCreateRecurringProformaFromProformaDefaultValues,
- buildCreateRecurringProformaFromProformaInput,
type CreateRecurringProformaFromProformaForm,
CreateRecurringProformaFromProformaFormSchema,
+ buildCreateRecurringProformaFromProformaDefaultValues,
+ buildCreateRecurringProformaFromProformaInput,
} from "./use-create-recurring-proforma-from-proforma-dialog-controller.utils";
const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection => {
@@ -41,6 +41,9 @@ export interface CreateRecurringProformaFromProformaDialogControllerParams {
proformaReference: string | null;
}
+type CreateRecurringProformaFromProformaTarget =
+ CreateRecurringProformaFromProformaDialogControllerParams;
+
export const useCreateRecurringProformaFromProformaDialogController = ({
proformaId,
proformaReference,
@@ -51,6 +54,10 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
const mutation = useCreateRecurringProformaFromProforma();
const [open, setOpen] = useState(false);
const submitInProgressRef = useRef(false);
+ const targetRef = useRef
({
+ proformaId,
+ proformaReference,
+ });
const form = useHookForm({
resolverSchema: CreateRecurringProformaFromProformaFormSchema,
initialValues: buildCreateRecurringProformaFromProformaDefaultValues(proformaReference),
@@ -77,8 +84,10 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
);
};
- const openDialog = () => {
- form.reset(buildCreateRecurringProformaFromProformaDefaultValues(proformaReference));
+ const openDialog = (target?: CreateRecurringProformaFromProformaTarget) => {
+ const nextTarget = target ?? { proformaId, proformaReference };
+ targetRef.current = nextTarget;
+ form.reset(buildCreateRecurringProformaFromProformaDefaultValues(nextTarget.proformaReference));
setOpen(true);
};
@@ -100,7 +109,7 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
try {
const created = await mutation.mutateAsync({
- proformaId,
+ proformaId: targetRef.current.proformaId,
data: buildCreateRecurringProformaFromProformaInput(values),
});
diff --git a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.test.tsx b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.test.tsx
index c6539118..9736c495 100644
--- a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.test.tsx
+++ b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.test.tsx
@@ -1,13 +1,14 @@
import assert from "node:assert/strict";
import test from "node:test";
-import { isValidElement } from "react";
-import { renderToStaticMarkup } from "react-dom/server";
-import type { CellContext } from "@tanstack/react-table";
import { RdxRowActions, type RdxRowActionsProps } from "@repo/rdx-ui/components";
+import type { CellContext } from "@tanstack/react-table";
+import { isValidElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
import type { ProformaListRow } from "../../../../shared";
import { PROFORMA_STATUS } from "../../../../shared";
+
import { useProformasGridColumns } from "./use-proforma-grid-columns";
const proformaFixture: ProformaListRow = {
@@ -104,12 +105,16 @@ test("mantiene las destructivas en overflow y conserva disabled", () => {
assert.equal(viewAction?.disabled, true);
});
-test("preserva callbacks de view, edit y delete", () => {
+test("preserva callbacks de view, edit, crear recurrencia y delete", () => {
let didView = false;
let didEdit = false;
+ let didCreateRecurring = false;
let didDelete = false;
const props = getRowActionsProps("normal", {
+ onCreateRecurringClick: () => {
+ didCreateRecurring = true;
+ },
onDeleteClick: () => {
didDelete = true;
},
@@ -123,9 +128,29 @@ test("preserva callbacks de view, edit y delete", () => {
props.primaryActions?.find((action) => action.id === "view")?.onSelect();
props.primaryActions?.find((action) => action.id === "edit")?.onSelect();
+ props.overflowActions?.find((action) => action.id === "create-recurring")?.onSelect();
props.overflowActions?.find((action) => action.id === "delete")?.onSelect();
assert.equal(didView, true);
assert.equal(didEdit, true);
+ assert.equal(didCreateRecurring, true);
assert.equal(didDelete, true);
});
+
+test("solo permite crear recurrencia desde el listado normal", () => {
+ const normalProps = getRowActionsProps("normal", {
+ onCreateRecurringClick: () => undefined,
+ });
+ const archivedProps = getRowActionsProps("archived", {
+ onCreateRecurringClick: () => undefined,
+ });
+
+ assert.equal(
+ normalProps.overflowActions?.find((action) => action.id === "create-recurring")?.disabled,
+ false
+ );
+ assert.equal(
+ archivedProps.overflowActions?.find((action) => action.id === "create-recurring")?.disabled,
+ true
+ );
+});
diff --git a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx
index 9be62aee..7afa405d 100644
--- a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx
+++ b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx
@@ -1,12 +1,10 @@
import { EmptyCellValue } from "@erp/core/components";
-import { DataTableColumnHeader, RdxRowActions, type RdxRowAction } from "@repo/rdx-ui/components";
+import { DataTableColumnHeader, type RdxRowAction, RdxRowActions } from "@repo/rdx-ui/components";
import { DateHelper } from "@repo/rdx-utils";
-import {
- Checkbox,
- Spinner,
-} from "@repo/shadcn-ui/components";
+import { Checkbox, Spinner } from "@repo/shadcn-ui/components";
import type { ColumnDef } from "@tanstack/react-table";
import {
+ CalendarSyncIcon,
CopyIcon,
ExternalLinkIcon,
EyeIcon,
@@ -37,6 +35,7 @@ type GridActionHandlers = {
onIssueClick?: (proforma: ProformaListRow) => void;
onArchiveClick?: (proforma: ProformaListRow) => void;
onDuplicateClick?: (proforma: ProformaListRow) => void;
+ onCreateRecurringClick?: (proforma: ProformaListRow) => void;
onUnarchiveClick?: (proforma: ProformaListRow) => void;
onDeleteClick?: (proforma: ProformaListRow) => void;
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void;
@@ -52,10 +51,8 @@ type GridActionHandlers = {
isPdfDownloading?: boolean;
};
-const renderActionIcon = (
- Icon: ComponentType<{ className?: string }>,
- loading?: boolean
-) => (loading ? : );
+const renderActionIcon = (Icon: ComponentType<{ className?: string }>, loading?: boolean) =>
+ loading ? : ;
export function useProformasGridColumns(
scope: ProformaListScope,
@@ -312,6 +309,7 @@ export function useProformasGridColumns(
const handleView = () => actionHandlers.onViewClick?.(proforma);
const handleEdit = () => actionHandlers.onEditClick?.(proforma);
const handleDuplicate = () => actionHandlers.onDuplicateClick?.(proforma);
+ const handleCreateRecurring = () => actionHandlers.onCreateRecurringClick?.(proforma);
const handleArchive = () => actionHandlers.onArchiveClick?.(proforma);
const handleUnarchive = () => actionHandlers.onUnarchiveClick?.(proforma);
const handleIssue = () => actionHandlers.onIssueClick?.(proforma);
@@ -416,6 +414,14 @@ export function useProformasGridColumns(
: undefined,
onSelect: handleDuplicate,
},
+ {
+ id: "create-recurring",
+ label: t("pages.proformas.list.actions.create_recurring"),
+ icon: renderActionIcon(CalendarSyncIcon),
+ disabled: !(actionHandlers.onCreateRecurringClick && isNormalScope),
+ disabledReason: isNormalScope ? undefined : getScopeReason(),
+ onSelect: handleCreateRecurring,
+ },
{
id: "archive",
label: t("pages.proformas.list.actions.archive"),
diff --git a/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx b/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx
index e7b3dddb..c0daf201 100644
--- a/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx
+++ b/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx
@@ -32,6 +32,8 @@ import { ChangeProformaStatusDialog } from "../../../change-status";
import { prepareChangeProformaStatusTargets } from "../../../change-status/utils";
import { DeleteProformaDialog } from "../../../delete";
import { prepareDeleteProformaTargets } from "../../../delete/utils";
+import { useCreateRecurringProformaFromProformaDialogController } from "../../../detail/controllers";
+import { CreateRecurringProformaFromProformaDialog } from "../../../detail/ui/components";
import { downloadProformaPDFApi } from "../../../download-pdf/api";
import { IssueProformaDialog } from "../../../issue-proforma";
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
@@ -238,6 +240,10 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
goHome,
isRetrying,
} = useListProformasPageController(scope);
+ const createRecurringDialogCtrl = useCreateRecurringProformaFromProformaDialogController({
+ proformaId: "",
+ proformaReference: null,
+ });
const handleEditClick = (proformaId: string) => {
navigate({
@@ -293,6 +299,11 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
onViewClick: (proforma) => handleOpenDetailClick(proforma.id),
onEditClick: (proforma) => handleEditClick(proforma.id),
onDuplicateClick: handleDuplicateClick,
+ onCreateRecurringClick: (proforma) =>
+ createRecurringDialogCtrl.openDialog({
+ proformaId: proforma.id,
+ proformaReference: proforma.proformaReference,
+ }),
onIssueClick: (proformaRow) =>
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
onArchiveClick: archiveDialogCtrl.openDialog,
@@ -550,6 +561,21 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
targets={changeStatusDialogCtrl.targets}
/>
+ {
+ if (!open) {
+ createRecurringDialogCtrl.closeDialog();
+ }
+ }}
+ onSubmit={createRecurringDialogCtrl.onSubmit}
+ onUnitChange={createRecurringDialogCtrl.changeUnit}
+ open={createRecurringDialogCtrl.open}
+ unit={createRecurringDialogCtrl.unit}
+ />
+
(
+
+
+
+
+
+
+
+);
+
+interface AccountErrorStateProps {
+ canRetry: boolean;
+ description: string;
+ isRetrying: boolean;
+ onRetry: () => void;
+ reference?: string;
+ retryLabel: string;
+ retryingLabel: string;
+ title: string;
+ variant: ComponentProps["variant"];
+}
+
+export const AccountErrorState = ({
+ canRetry,
+ description,
+ isRetrying,
+ onRetry,
+ reference,
+ retryLabel,
+ retryingLabel,
+ title,
+ variant,
+}: AccountErrorStateProps) => (
+
+
+
+
+
+
+
+);
diff --git a/modules/identity/src/web/account/ui/pages/account-page.test.tsx b/modules/identity/src/web/account/ui/pages/account-page.test.tsx
new file mode 100644
index 00000000..fc72fed6
--- /dev/null
+++ b/modules/identity/src/web/account/ui/pages/account-page.test.tsx
@@ -0,0 +1,36 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { renderToStaticMarkup } from "react-dom/server";
+
+import { AccountErrorState, AccountLoadingState } from "./account-page-states";
+
+test("Account loading usa el frame y contenedor definitivos", () => {
+ const html = renderToStaticMarkup();
+
+ assert.match(html, /flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden/);
+ assert.match(html, /rdx-page-container/);
+ assert.match(html, /Cargando cuenta/);
+ assert.doesNotMatch(html, /fixed inset-0/);
+ assert.doesNotMatch(html, /rdx-editor-page-layout/);
+});
+
+test("Account error permanece dentro del frame actual", () => {
+ const html = renderToStaticMarkup(
+ undefined}
+ retryingLabel="Reintentando"
+ retryLabel="Reintentar"
+ title="Error de cuenta"
+ variant="server"
+ />
+ );
+
+ assert.match(html, /flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden/);
+ assert.match(html, /Error de cuenta/);
+ assert.match(html, /Reintentar/);
+ assert.doesNotMatch(html, /fixed inset-0/);
+});
diff --git a/modules/identity/src/web/account/ui/pages/account-page.tsx b/modules/identity/src/web/account/ui/pages/account-page.tsx
index 70ba63af..e376088d 100644
--- a/modules/identity/src/web/account/ui/pages/account-page.tsx
+++ b/modules/identity/src/web/account/ui/pages/account-page.tsx
@@ -1,10 +1,12 @@
-import { ErrorAlert, PageErrorState } from "@erp/core/components";
+import { ErrorAlert } from "@erp/core/components";
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
import {
CancelActionButton,
RdxEditorActionBar,
RdxEditorHeader,
- RdxEditorPageLayout,
+ RdxEditorLayout,
+ RdxPageContainer,
+ RdxPageFrame,
RdxPanel,
RhfSubmitActionButton,
SelectField,
@@ -27,65 +29,7 @@ import { useTranslation } from "../../../i18n";
import { useAccountPageController } from "../../controllers";
import type { AccountPasswordForm, AccountProfileForm } from "../../entities";
-const AccountLoadingState = ({ label }: { label: string }) => {
- return (
-
- );
-};
-
-const AccountErrorState = ({
- canRetry,
- description,
- isRetrying,
- onRetry,
- reference,
- retryLabel,
- retryingLabel,
- title,
- variant,
-}: {
- canRetry: boolean;
- description: string;
- isRetrying: boolean;
- onRetry: () => void;
- reference?: string;
- retryLabel: string;
- retryingLabel: string;
- title: string;
- variant: React.ComponentProps["variant"];
-}) => {
- return (
-
- );
-};
+import { AccountErrorState, AccountLoadingState } from "./account-page-states";
const AccountPasswordDialog = ({
ctrl,
@@ -252,15 +196,18 @@ export const AccountPage = () => {
};
return (
-
-
-
-
-
+
+ }
+ >
+
+
@@ -405,8 +352,8 @@ export const AccountPage = () => {
-
-
+
+
{
onOpenChange={handlePasswordDialogOpenChange}
t={t}
/>
-
+
);
};
diff --git a/packages/rdx-ui/src/components/editor-page/index.ts b/packages/rdx-ui/src/components/editor-page/index.ts
index d5836766..18a2fe04 100644
--- a/packages/rdx-ui/src/components/editor-page/index.ts
+++ b/packages/rdx-ui/src/components/editor-page/index.ts
@@ -1,5 +1,4 @@
export * from "./rdx-editor-action-bar.tsx";
export * from "./rdx-editor-header.tsx";
export * from "./rdx-editor-layout.tsx";
-export * from "./rdx-editor-page-layout.tsx";
export * from "./rdx-panel.tsx";
diff --git a/packages/rdx-ui/src/components/editor-page/rdx-editor-foundations.test.tsx b/packages/rdx-ui/src/components/editor-page/rdx-editor-foundations.test.tsx
index c90ad484..ec5633c7 100644
--- a/packages/rdx-ui/src/components/editor-page/rdx-editor-foundations.test.tsx
+++ b/packages/rdx-ui/src/components/editor-page/rdx-editor-foundations.test.tsx
@@ -8,7 +8,6 @@ import { RdxFormGrid } from "../form/rdx-form-grid.tsx";
import { RdxEditorActionBar } from "./rdx-editor-action-bar.tsx";
import { RdxEditorHeader } from "./rdx-editor-header.tsx";
import { RdxEditorLayout } from "./rdx-editor-layout.tsx";
-import { RdxEditorPageLayout } from "./rdx-editor-page-layout.tsx";
test("RdxEditorLayout only distributes main content and an optional aside", () => {
const html = renderToStaticMarkup(
@@ -50,44 +49,6 @@ test("RdxEditorHeader no renderiza back sin callback", () => {
import { RdxPanel } from "./rdx-panel.tsx";
-test("RdxEditorPageLayout aplica complejidad simple por defecto y renderiza children", () => {
- const html = renderToStaticMarkup(
-
- Editor content
-
- );
-
- assert.match(html, /Editor content/);
- assert.match(html, /max-w-5xl/);
- assert.match(html, /px-4/);
- assert.match(html, /md:px-6/);
-});
-
-test("RdxEditorPageLayout soporta aside opcional y complejidad complex", () => {
- const html = renderToStaticMarkup(
- Aside content } width="full">
-