diff --git a/apps/web/src/development/ui-kit/ui-kit-page.tsx b/apps/web/src/development/ui-kit/ui-kit-page.tsx index eb4b13d3..d611f0c4 100644 --- a/apps/web/src/development/ui-kit/ui-kit-page.tsx +++ b/apps/web/src/development/ui-kit/ui-kit-page.tsx @@ -2,7 +2,7 @@ import { RdxBadge, RdxContextPanel, RdxEditorActionBar, - RdxEditorPageLayout, + RdxEditorLayout, RdxFormGrid, RdxPageActions, RdxPageContainer, @@ -751,13 +751,15 @@ export default function UiKitPage() { title="Editor foundations" >
- +
- -
- Centered editor content -
-
+ + +
+ Centered editor content +
+
+
@@ -806,47 +808,49 @@ export default function UiKitPage() { title="Simple editor composition" >
- -
-
-
- Save error aligned with editor width -
- + + +
- } - title="Basic information" - > - -
- Name field -
-
- Tax ID field -
-
-
+
+ Save error aligned with editor width +
- } - title="Preferences" - > - -
- Language field -
-
- Currency field -
-
-
+
+ } + title="Basic information" + > + +
+ Name field +
+
+ Tax ID field +
+
+
+ + } + title="Preferences" + > + +
+ Language field +
+
+ Currency field +
+
+
+
-
- + +
@@ -870,61 +874,65 @@ export default function UiKitPage() {
- - -
- 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} + /> + ( + + + +
+
+ {label} +
+
+
+
+
+); + +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 ( -
- -
-
- {label} -
-
-
-
- ); -}; - -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"> -
Main content
-
- ); - - assert.match(html, /Main content/); - assert.match(html, /Aside content/); - assert.match(html, /xl:grid-cols-\[minmax\(0,1fr\)_22rem\]/); - assert.doesNotMatch(html, /max-w-5xl/); -}); - -test("RdxEditorPageLayout no reserva aside cuando no existe incluso en complex", () => { - const html = renderToStaticMarkup( - -
Main content
-
- ); - - assert.match(html, /Main content/); - assert.doesNotMatch(html, /