Compare commits
4 Commits
3d2ce8b981
...
3aa3f4018b
| Author | SHA1 | Date | |
|---|---|---|---|
| 3aa3f4018b | |||
| 2f8122ca77 | |||
| 85d2f11bde | |||
| 3eb7c37a7b |
@ -2,7 +2,7 @@ import {
|
|||||||
RdxBadge,
|
RdxBadge,
|
||||||
RdxContextPanel,
|
RdxContextPanel,
|
||||||
RdxEditorActionBar,
|
RdxEditorActionBar,
|
||||||
RdxEditorPageLayout,
|
RdxEditorLayout,
|
||||||
RdxFormGrid,
|
RdxFormGrid,
|
||||||
RdxPageActions,
|
RdxPageActions,
|
||||||
RdxPageContainer,
|
RdxPageContainer,
|
||||||
@ -751,13 +751,15 @@ export default function UiKitPage() {
|
|||||||
title="Editor foundations"
|
title="Editor foundations"
|
||||||
>
|
>
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<ExampleCard title="RdxEditorPageLayout simple">
|
<ExampleCard title="RdxPageContainer + RdxEditorLayout">
|
||||||
<div className="rounded-lg border border-border bg-muted/30">
|
<div className="rounded-lg border border-border bg-muted/30">
|
||||||
<RdxEditorPageLayout complexity="simple">
|
<RdxPageContainer width="content">
|
||||||
<div className="rounded-lg border border-dashed border-border bg-background p-4 text-sm text-foreground">
|
<RdxEditorLayout>
|
||||||
Centered editor content
|
<div className="rounded-lg border border-dashed border-border bg-background p-4 text-sm text-foreground">
|
||||||
</div>
|
Centered editor content
|
||||||
</RdxEditorPageLayout>
|
</div>
|
||||||
|
</RdxEditorLayout>
|
||||||
|
</RdxPageContainer>
|
||||||
</div>
|
</div>
|
||||||
</ExampleCard>
|
</ExampleCard>
|
||||||
|
|
||||||
@ -806,47 +808,49 @@ export default function UiKitPage() {
|
|||||||
title="Simple editor composition"
|
title="Simple editor composition"
|
||||||
>
|
>
|
||||||
<div className="rounded-lg border border-border bg-muted/30">
|
<div className="rounded-lg border border-border bg-muted/30">
|
||||||
<RdxEditorPageLayout complexity="simple">
|
<RdxPageContainer width="content">
|
||||||
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
<RdxEditorLayout>
|
||||||
<div className="flex min-w-0 flex-col gap-6">
|
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
||||||
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
|
|
||||||
Save error aligned with editor width
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-col gap-6">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
<RdxPanel
|
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
|
||||||
description="Primary customer information"
|
Save error aligned with editor width
|
||||||
icon={<PlusIcon className="size-4" />}
|
</div>
|
||||||
title="Basic information"
|
|
||||||
>
|
|
||||||
<RdxFormGrid className="md:grid-cols-2">
|
|
||||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
|
||||||
Name field
|
|
||||||
</div>
|
|
||||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
|
||||||
Tax ID field
|
|
||||||
</div>
|
|
||||||
</RdxFormGrid>
|
|
||||||
</RdxPanel>
|
|
||||||
|
|
||||||
<RdxPanel
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
description="Secondary preferences"
|
<RdxPanel
|
||||||
icon={<PlusIcon className="size-4" />}
|
description="Primary customer information"
|
||||||
title="Preferences"
|
icon={<PlusIcon className="size-4" />}
|
||||||
>
|
title="Basic information"
|
||||||
<RdxFormGrid className="md:grid-cols-2">
|
>
|
||||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
<RdxFormGrid className="md:grid-cols-2">
|
||||||
Language field
|
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
||||||
</div>
|
Name field
|
||||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
</div>
|
||||||
Currency field
|
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
||||||
</div>
|
Tax ID field
|
||||||
</RdxFormGrid>
|
</div>
|
||||||
</RdxPanel>
|
</RdxFormGrid>
|
||||||
|
</RdxPanel>
|
||||||
|
|
||||||
|
<RdxPanel
|
||||||
|
description="Secondary preferences"
|
||||||
|
icon={<PlusIcon className="size-4" />}
|
||||||
|
title="Preferences"
|
||||||
|
>
|
||||||
|
<RdxFormGrid className="md:grid-cols-2">
|
||||||
|
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
||||||
|
Language field
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm">
|
||||||
|
Currency field
|
||||||
|
</div>
|
||||||
|
</RdxFormGrid>
|
||||||
|
</RdxPanel>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</RdxEditorLayout>
|
||||||
</RdxEditorPageLayout>
|
</RdxPageContainer>
|
||||||
</div>
|
</div>
|
||||||
</ExampleCard>
|
</ExampleCard>
|
||||||
|
|
||||||
@ -870,61 +874,65 @@ export default function UiKitPage() {
|
|||||||
|
|
||||||
<div className="flex min-h-0 flex-1 flex-col bg-muted/20">
|
<div className="flex min-h-0 flex-1 flex-col bg-muted/20">
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||||
<RdxEditorPageLayout
|
<RdxPageContainer width="full">
|
||||||
aside={
|
<RdxEditorLayout
|
||||||
<div className="flex min-w-0 flex-col gap-6">
|
aside={
|
||||||
<RdxPanel description="Secondary recipient controls." title="Recipient">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
|
<RdxPanel
|
||||||
Customer selector and helper actions
|
description="Secondary recipient controls."
|
||||||
</div>
|
title="Recipient"
|
||||||
</RdxPanel>
|
>
|
||||||
|
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
|
||||||
|
Customer selector and helper actions
|
||||||
|
</div>
|
||||||
|
</RdxPanel>
|
||||||
|
|
||||||
<RdxPanel
|
<RdxPanel
|
||||||
description="Fiscal defaults and toggles."
|
description="Fiscal defaults and toggles."
|
||||||
title="Tax settings"
|
title="Tax settings"
|
||||||
>
|
>
|
||||||
<RdxFormGrid>
|
<RdxFormGrid>
|
||||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-12">
|
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-12">
|
||||||
Tax regime field
|
Tax regime field
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-12">
|
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-12">
|
||||||
Default tax selector
|
Default tax selector
|
||||||
</div>
|
</div>
|
||||||
</RdxFormGrid>
|
</RdxFormGrid>
|
||||||
</RdxPanel>
|
</RdxPanel>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
complexity="complex"
|
|
||||||
>
|
|
||||||
<div className="rounded-2xl border border-border bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
|
||||||
<div className="flex min-w-0 flex-col gap-6">
|
|
||||||
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
|
|
||||||
Inline feedback aligned with the editor content
|
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<RdxPanel title="Document information">
|
>
|
||||||
<RdxFormGrid>
|
<div className="rounded-2xl border border-border bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
||||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
Reference field
|
<div className="rounded-lg border border-border bg-background px-4 py-3 text-sm text-muted-foreground">
|
||||||
</div>
|
Inline feedback aligned with the editor content
|
||||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
|
||||||
Date field
|
|
||||||
</div>
|
|
||||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
|
||||||
Series field
|
|
||||||
</div>
|
|
||||||
</RdxFormGrid>
|
|
||||||
</RdxPanel>
|
|
||||||
|
|
||||||
<section className="min-w-0 border-b border-border pb-6">
|
|
||||||
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
|
|
||||||
Line editor keeps its own internal table layout and stays in the
|
|
||||||
main surface.
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
<RdxPanel title="Document information">
|
||||||
|
<RdxFormGrid>
|
||||||
|
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
||||||
|
Reference field
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
||||||
|
Date field
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-dashed border-border bg-muted/30 p-3 text-sm md:col-span-4">
|
||||||
|
Series field
|
||||||
|
</div>
|
||||||
|
</RdxFormGrid>
|
||||||
|
</RdxPanel>
|
||||||
|
|
||||||
|
<section className="min-w-0 border-b border-border pb-6">
|
||||||
|
<div className="rounded-md border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
|
||||||
|
Line editor keeps its own internal table layout and stays in the
|
||||||
|
main surface.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</RdxEditorLayout>
|
||||||
</RdxEditorPageLayout>
|
</RdxPageContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RdxEditorActionBar
|
<RdxEditorActionBar
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { PageErrorState } from "@erp/core/components";
|
import { PageErrorState } from "@erp/core/components";
|
||||||
import { RdxPageContainer, RdxPageHeader, RdxPageStack } from "@repo/rdx-ui/components";
|
import {
|
||||||
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
|
RdxPageHeader,
|
||||||
|
RdxPageStack,
|
||||||
|
} from "@repo/rdx-ui/components";
|
||||||
import { Skeleton } from "@repo/shadcn-ui/components";
|
import { Skeleton } from "@repo/shadcn-ui/components";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
@ -28,6 +33,7 @@ const HomeKpisSkeleton = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Renderiza el dashboard dentro del frame estándar de página. */
|
||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const { t } = useAppTranslation();
|
const { t } = useAppTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -35,8 +41,8 @@ export const HomePage = () => {
|
|||||||
|
|
||||||
if (ctrl.hasBlockingError && ctrl.pageError) {
|
if (ctrl.hasBlockingError && ctrl.pageError) {
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<div className="mx-auto w-full max-w-3xl">
|
<RdxPageContainer width="content">
|
||||||
<PageErrorState
|
<PageErrorState
|
||||||
description={ctrl.pageError.description}
|
description={ctrl.pageError.description}
|
||||||
primaryAction={{
|
primaryAction={{
|
||||||
@ -50,63 +56,65 @@ export const HomePage = () => {
|
|||||||
title={ctrl.pageError.title}
|
title={ctrl.pageError.title}
|
||||||
variant="server"
|
variant="server"
|
||||||
/>
|
/>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</RdxPageContainer>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<RdxPageStack spacing="default">
|
<RdxPageContainer width="full">
|
||||||
<RdxPageHeader
|
<RdxPageStack spacing="default">
|
||||||
description={t("web.home.header.description")}
|
<RdxPageHeader
|
||||||
title={t("web.home.header.title")}
|
description={t("web.home.header.description")}
|
||||||
/>
|
title={t("web.home.header.title")}
|
||||||
|
/>
|
||||||
|
|
||||||
{ctrl.customersTotalQuery.isLoading ||
|
{ctrl.customersTotalQuery.isLoading ||
|
||||||
ctrl.proformasOverviewQuery.isLoading ||
|
ctrl.proformasOverviewQuery.isLoading ||
|
||||||
ctrl.monthlyBillingQuery.isLoading ? (
|
ctrl.monthlyBillingQuery.isLoading ? (
|
||||||
<HomeKpisSkeleton />
|
<HomeKpisSkeleton />
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
{ctrl.kpis.map((kpi) => (
|
{ctrl.kpis.map((kpi) => (
|
||||||
<HomeKpiCard item={kpi} key={kpi.id} />
|
<HomeKpiCard item={kpi} key={kpi.id} />
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
||||||
|
<HomeAttentionPanel
|
||||||
|
hasError={ctrl.proformasOverviewQuery.isError}
|
||||||
|
items={ctrl.attentionItems}
|
||||||
|
/>
|
||||||
|
<HomeProformaStatusSummary
|
||||||
|
hasError={ctrl.proformasOverviewQuery.isError}
|
||||||
|
items={ctrl.proformaStatusSummary}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
||||||
<HomeAttentionPanel
|
<HomeRecentActivity
|
||||||
hasError={ctrl.proformasOverviewQuery.isError}
|
hasPartialError={ctrl.recentActivityQuery.hasPartialError}
|
||||||
items={ctrl.attentionItems}
|
items={ctrl.recentActivityQuery.items}
|
||||||
/>
|
/>
|
||||||
<HomeProformaStatusSummary
|
<HomeMonthlyBillingSummary
|
||||||
hasError={ctrl.proformasOverviewQuery.isError}
|
data={ctrl.monthlyBillingQuery.data}
|
||||||
items={ctrl.proformaStatusSummary}
|
hasError={ctrl.monthlyBillingQuery.isError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)]">
|
<p className="text-xs text-muted-foreground">
|
||||||
<HomeRecentActivity
|
{t("web.home.footer.note")}{" "}
|
||||||
hasPartialError={ctrl.recentActivityQuery.hasPartialError}
|
<Link
|
||||||
items={ctrl.recentActivityQuery.items}
|
className="underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
/>
|
to="/customer-invoices"
|
||||||
<HomeMonthlyBillingSummary
|
>
|
||||||
data={ctrl.monthlyBillingQuery.data}
|
{t("web.home.footer.link")}
|
||||||
hasError={ctrl.monthlyBillingQuery.isError}
|
</Link>
|
||||||
/>
|
</p>
|
||||||
</div>
|
</RdxPageStack>
|
||||||
|
</RdxPageContainer>
|
||||||
<p className="text-xs text-muted-foreground">
|
</RdxPageFrame>
|
||||||
{t("web.home.footer.note")}{" "}
|
|
||||||
<Link
|
|
||||||
className="underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
to="/customer-invoices"
|
|
||||||
>
|
|
||||||
{t("web.home.footer.link")}
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</RdxPageStack>
|
|
||||||
</RdxPageContainer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,8 +12,7 @@ export const AppFullscreenLayoutFrame = ({ children }: AppFullscreenLayoutFrameP
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shell de aplicación para páginas que ocupan toda el área disponible.
|
* Shell para páginas fullscreen sin sidebar ni topbar global.
|
||||||
* Se utiliza en editores y vistas de detalle sin sidebar ni topbar global.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const AppFullscreenLayout = () => {
|
export const AppFullscreenLayout = () => {
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
import { RdxPageContainer, RdxPageHeader, RdxPageStack } from "@repo/rdx-ui/components";
|
import {
|
||||||
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
|
RdxPageHeader,
|
||||||
|
RdxPageStack,
|
||||||
|
} from "@repo/rdx-ui/components";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
import type { SettingsNavigationSection } from "@/settings/build-settings-navigation";
|
import type { SettingsNavigationSection } from "@/settings/build-settings-navigation";
|
||||||
@ -11,27 +16,32 @@ interface SettingsLayoutProps {
|
|||||||
sections: SettingsNavigationSection[];
|
sections: SettingsNavigationSection[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compone la navegación y el contenido de configuración dentro del frame estándar de página.
|
||||||
|
*/
|
||||||
export const SettingsLayout = ({ sections }: SettingsLayoutProps) => {
|
export const SettingsLayout = ({ sections }: SettingsLayoutProps) => {
|
||||||
const { t } = useAppTranslation();
|
const { t } = useAppTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<RdxPageStack spacing="default">
|
<RdxPageContainer width="full">
|
||||||
<RdxPageHeader
|
<RdxPageStack spacing="default">
|
||||||
description={t("web.settings.description")}
|
<RdxPageHeader
|
||||||
title={t("web.settings.title")}
|
description={t("web.settings.description")}
|
||||||
/>
|
title={t("web.settings.title")}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="grid gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]">
|
<div className="grid gap-6 lg:grid-cols-[18rem_minmax(0,1fr)]">
|
||||||
<aside className="rounded-xl border border-border bg-card p-4 shadow-sm lg:sticky lg:top-6 lg:self-start">
|
<aside className="rounded-xl border border-border bg-card p-4 shadow-sm lg:sticky lg:top-6 lg:self-start">
|
||||||
<SettingsNavigation sections={sections} />
|
<SettingsNavigation sections={sections} />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section className="min-w-0">
|
<section className="min-w-0">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</RdxPageStack>
|
</RdxPageStack>
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { ErrorAlert, PageErrorState, SimpleSearchInput } from "@erp/core/components";
|
import { ErrorAlert, PageErrorState, SimpleSearchInput } from "@erp/core/components";
|
||||||
import { RdxPageContainer, RdxPageHeader, RdxPageStack } from "@repo/rdx-ui/components";
|
import {
|
||||||
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
|
RdxPageHeader,
|
||||||
|
RdxPageStack,
|
||||||
|
} from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Spinner,
|
Spinner,
|
||||||
@ -18,6 +23,7 @@ import { AuditFilterChips, AuditFiltersPanel } from "./audit-filters";
|
|||||||
import { AuditGrid } from "./audit-grid";
|
import { AuditGrid } from "./audit-grid";
|
||||||
import { useAuditGridColumns } from "./use-audit-grid-columns";
|
import { useAuditGridColumns } from "./use-audit-grid-columns";
|
||||||
|
|
||||||
|
/** Compone el listado de auditoría dentro del frame estándar de página. */
|
||||||
export const AuditPage = () => {
|
export const AuditPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const controller = useAuditPageController();
|
const controller = useAuditPageController();
|
||||||
@ -28,68 +34,72 @@ export const AuditPage = () => {
|
|||||||
|
|
||||||
if (controller.isError && !controller.hasUsableData) {
|
if (controller.isError && !controller.hasUsableData) {
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<PageErrorState
|
<RdxPageContainer width="content">
|
||||||
description={t("audit.error.load")}
|
<PageErrorState
|
||||||
primaryAction={{
|
description={t("audit.error.load")}
|
||||||
label: t("audit.retry"),
|
primaryAction={{
|
||||||
onClick: retry,
|
label: t("audit.retry"),
|
||||||
isLoading: controller.isFetching,
|
onClick: retry,
|
||||||
}}
|
isLoading: controller.isFetching,
|
||||||
title={t("audit.error.load")}
|
}}
|
||||||
variant="unknown"
|
title={t("audit.error.load")}
|
||||||
/>
|
variant="unknown"
|
||||||
</RdxPageContainer>
|
/>
|
||||||
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<RdxPageStack spacing="default">
|
<RdxPageContainer width="full">
|
||||||
<RdxPageHeader description={t("audit.description")} title={t("audit.title")} />
|
<RdxPageStack spacing="default">
|
||||||
|
<RdxPageHeader description={t("audit.description")} title={t("audit.title")} />
|
||||||
{controller.isError && controller.hasUsableData ? (
|
|
||||||
<ErrorAlert message={t("audit.error.refresh")} title={t("audit.error.load")} />
|
{controller.isError && controller.hasUsableData ? (
|
||||||
) : null}
|
<ErrorAlert message={t("audit.error.refresh")} title={t("audit.error.load")} />
|
||||||
|
) : null}
|
||||||
<div className="flex flex-col gap-4 lg:flex-row">
|
|
||||||
<AuditFiltersPanel filters={controller.filters} onChange={controller.setFilters} />
|
<div className="flex flex-col gap-4 lg:flex-row">
|
||||||
<div className="min-w-0 flex-1 space-y-4">
|
<AuditFiltersPanel filters={controller.filters} onChange={controller.setFilters} />
|
||||||
<AuditToolbar controller={controller} />
|
<div className="min-w-0 flex-1 space-y-4">
|
||||||
<AuditFilterChips
|
<AuditToolbar controller={controller} />
|
||||||
filters={controller.filters}
|
<AuditFilterChips
|
||||||
onChange={controller.setFilters}
|
filters={controller.filters}
|
||||||
onReset={controller.resetFilters}
|
onChange={controller.setFilters}
|
||||||
/>
|
onReset={controller.resetFilters}
|
||||||
{!controller.isLoading && controller.data.items.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted-foreground" role="status">
|
|
||||||
{controller.filters.q ||
|
|
||||||
controller.filters.kind ||
|
|
||||||
controller.filters.targetType ||
|
|
||||||
controller.filters.action
|
|
||||||
? t("audit.empty_filtered")
|
|
||||||
: t("audit.empty")}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<AuditGrid
|
|
||||||
columns={columns}
|
|
||||||
data={controller.data}
|
|
||||||
fetching={controller.isFetching}
|
|
||||||
loading={controller.isLoading}
|
|
||||||
onOrderChange={controller.setOrder}
|
|
||||||
onPageChange={controller.setPageNumber}
|
|
||||||
onPageSizeChange={controller.setPageSize}
|
|
||||||
onRowClick={controller.selectEntry}
|
|
||||||
order={controller.order}
|
|
||||||
pageNumber={controller.pageNumber}
|
|
||||||
pageSize={controller.pageSize}
|
|
||||||
/>
|
/>
|
||||||
)}
|
{!controller.isLoading && controller.data.items.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground" role="status">
|
||||||
|
{controller.filters.q ||
|
||||||
|
controller.filters.kind ||
|
||||||
|
controller.filters.targetType ||
|
||||||
|
controller.filters.action
|
||||||
|
? t("audit.empty_filtered")
|
||||||
|
: t("audit.empty")}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<AuditGrid
|
||||||
|
columns={columns}
|
||||||
|
data={controller.data}
|
||||||
|
fetching={controller.isFetching}
|
||||||
|
loading={controller.isLoading}
|
||||||
|
onOrderChange={controller.setOrder}
|
||||||
|
onPageChange={controller.setPageNumber}
|
||||||
|
onPageSizeChange={controller.setPageSize}
|
||||||
|
onRowClick={controller.selectEntry}
|
||||||
|
order={controller.order}
|
||||||
|
pageNumber={controller.pageNumber}
|
||||||
|
pageSize={controller.pageSize}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<AuditEntrySheet entryId={controller.entry} onClose={controller.closeEntry} />
|
||||||
<AuditEntrySheet entryId={controller.entry} onClose={controller.closeEntry} />
|
</RdxPageStack>
|
||||||
</RdxPageStack>
|
</RdxPageContainer>
|
||||||
</RdxPageContainer>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Card, CardContent, RdxBadge } from "@repo/shadcn-ui/components";
|
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||||
|
import { Card, CardContent } from "@repo/shadcn-ui/components";
|
||||||
import { CalendarClockIcon } from "lucide-react";
|
import { CalendarClockIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { PageErrorState } from "@erp/core/components";
|
import { PageErrorState } from "@erp/core/components";
|
||||||
import { RdxPageContainer } from "@repo/rdx-ui/components";
|
import { RdxBadge, RdxPageContainer } from "@repo/rdx-ui/components";
|
||||||
import { Button, Card, CardContent, RdxBadge } from "@repo/shadcn-ui/components";
|
import { Button, Card, CardContent } from "@repo/shadcn-ui/components";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export * from './page-form-header';
|
export * from "./page-keyboard-shortcuts-button";
|
||||||
export * from './page-keyboard-shortcuts-button';
|
|
||||||
|
|||||||
@ -2,14 +2,6 @@ import type { ModuleClientParams } from "@erp/core/client";
|
|||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
import { Outlet, type RouteObject } from "react-router-dom";
|
import { Outlet, type RouteObject } from "react-router-dom";
|
||||||
|
|
||||||
const ProformaLayout = lazy(() =>
|
|
||||||
import("./proformas/shared/ui").then((m) => ({ default: m.ProformaLayout }))
|
|
||||||
);
|
|
||||||
|
|
||||||
const RecurringProformaLayout = lazy(() =>
|
|
||||||
import("./recurring-proformas/shared/ui").then((m) => ({ default: m.RecurringProformaLayout }))
|
|
||||||
);
|
|
||||||
|
|
||||||
const ProformasListPage = lazy(() =>
|
const ProformasListPage = lazy(() =>
|
||||||
import("./proformas/list").then((m) => ({ default: m.ListProformasPage }))
|
import("./proformas/list").then((m) => ({ default: m.ListProformasPage }))
|
||||||
);
|
);
|
||||||
@ -46,10 +38,6 @@ const RecurringProformaDetailPage = lazy(() =>
|
|||||||
import("./recurring-proformas").then((m) => ({ default: m.RecurringProformaDetailPage }))
|
import("./recurring-proformas").then((m) => ({ default: m.RecurringProformaDetailPage }))
|
||||||
);
|
);
|
||||||
|
|
||||||
const IssuedInvoicesLayout = lazy(() =>
|
|
||||||
import("./issued-invoices/shared/ui").then((m) => ({ default: m.IssuedInvoicesLayout }))
|
|
||||||
);
|
|
||||||
|
|
||||||
const IssuedInvoiceListPage = lazy(() =>
|
const IssuedInvoiceListPage = lazy(() =>
|
||||||
import("./issued-invoices/list").then((m) => ({ default: m.ListIssuedInvoicesPage }))
|
import("./issued-invoices/list").then((m) => ({ default: m.ListIssuedInvoicesPage }))
|
||||||
);
|
);
|
||||||
@ -70,11 +58,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
layout: "app-sidebar",
|
layout: "app-sidebar",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <Outlet context={params} />,
|
||||||
<ProformaLayout>
|
|
||||||
<Outlet context={params} />
|
|
||||||
</ProformaLayout>
|
|
||||||
),
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
@ -101,11 +85,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <ProformasCreatePage />,
|
||||||
<ProformaLayout>
|
|
||||||
<ProformasCreatePage />
|
|
||||||
</ProformaLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -114,11 +94,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <ProformaUpdatePage />,
|
||||||
<ProformaLayout>
|
|
||||||
<ProformaUpdatePage />
|
|
||||||
</ProformaLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -127,11 +103,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <ProformaDetailPage />,
|
||||||
<ProformaLayout>
|
|
||||||
<ProformaDetailPage />
|
|
||||||
</ProformaLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "recurring-proformas",
|
path: "recurring-proformas",
|
||||||
@ -139,11 +111,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
layout: "app-sidebar",
|
layout: "app-sidebar",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <RecurringProformasListPage />,
|
||||||
<RecurringProformaLayout>
|
|
||||||
<RecurringProformasListPage />
|
|
||||||
</RecurringProformaLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "recurring-proformas/archived",
|
path: "recurring-proformas/archived",
|
||||||
@ -151,11 +119,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
layout: "app-sidebar",
|
layout: "app-sidebar",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <RecurringProformasListPage />,
|
||||||
<RecurringProformaLayout>
|
|
||||||
<RecurringProformasListPage />
|
|
||||||
</RecurringProformaLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "recurring-proformas/create",
|
path: "recurring-proformas/create",
|
||||||
@ -163,11 +127,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <RecurringProformaCreatePage />,
|
||||||
<RecurringProformaLayout>
|
|
||||||
<RecurringProformaCreatePage />
|
|
||||||
</RecurringProformaLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "recurring-proformas/:recurring_proforma_id",
|
path: "recurring-proformas/:recurring_proforma_id",
|
||||||
@ -175,11 +135,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <RecurringProformaDetailPage />,
|
||||||
<RecurringProformaLayout>
|
|
||||||
<RecurringProformaDetailPage />
|
|
||||||
</RecurringProformaLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -188,11 +144,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
layout: "app-sidebar",
|
layout: "app-sidebar",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <Outlet context={params} />,
|
||||||
<IssuedInvoicesLayout>
|
|
||||||
<Outlet context={params} />
|
|
||||||
</IssuedInvoicesLayout>
|
|
||||||
),
|
|
||||||
children: [
|
children: [
|
||||||
{ path: "", index: true, element: <IssuedInvoiceListPage /> }, // index
|
{ path: "", index: true, element: <IssuedInvoiceListPage /> }, // index
|
||||||
{ path: "list", element: <IssuedInvoiceListPage /> },
|
{ path: "list", element: <IssuedInvoiceListPage /> },
|
||||||
@ -216,11 +168,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <IssuedInvoiceDetailPage />,
|
||||||
<IssuedInvoicesLayout>
|
|
||||||
<IssuedInvoiceDetailPage />
|
|
||||||
</IssuedInvoicesLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "settings/billing/verifactu",
|
path: "settings/billing/verifactu",
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export const IssuedInvoiceDetailContent = ({
|
|||||||
inspector,
|
inspector,
|
||||||
}: IssuedInvoiceDetailContentProps) => {
|
}: IssuedInvoiceDetailContentProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-0 flex-1 overflow-auto lg:overflow-hidden">
|
<div className="min-h-full lg:h-full lg:min-h-0 lg:overflow-hidden">
|
||||||
<div className="grid min-h-full grid-cols-1 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_340px] lg:overflow-hidden xl:grid-cols-[minmax(0,1fr)_380px]">
|
<div className="grid min-h-full grid-cols-1 lg:h-full lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_340px] lg:overflow-hidden xl:grid-cols-[minmax(0,1fr)_380px]">
|
||||||
{documentPane}
|
{documentPane}
|
||||||
{inspector}
|
{inspector}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { PageFormHeader } from "@erp/core/components";
|
import { RdxEditorHeader } from "@repo/rdx-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
@ -26,17 +26,17 @@ export const IssuedInvoicePageHeader = ({
|
|||||||
contentClassName,
|
contentClassName,
|
||||||
}: IssuedInvoicePageHeaderProps) => {
|
}: IssuedInvoicePageHeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<PageFormHeader
|
<RdxEditorHeader
|
||||||
actions={actions}
|
actions={actions}
|
||||||
backLabel={backLabel}
|
backLabel={backLabel}
|
||||||
className={cn("shrink-0", className)}
|
className={cn("shrink-0", className)}
|
||||||
contentClassName={contentClassName}
|
contentClassName={contentClassName}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
showStatus={showStatus}
|
showStatus={showStatus}
|
||||||
statusLabel={statusLabel}
|
status={statusLabel}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</PageFormHeader>
|
</RdxEditorHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { PageErrorState } from "@erp/core/components";
|
import { PageErrorState } from "@erp/core/components";
|
||||||
|
import { RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import { useIssuedInvoiceDetailPageController } from "../../controllers";
|
import { useIssuedInvoiceDetailPageController } from "../../controllers";
|
||||||
@ -20,8 +21,8 @@ export const IssuedInvoiceDetailPage = () => {
|
|||||||
|
|
||||||
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
|
<PageErrorState
|
||||||
description={ctrl.state.pageError.description}
|
description={ctrl.state.pageError.description}
|
||||||
primaryAction={ctrl.state.pageError.primaryAction}
|
primaryAction={ctrl.state.pageError.primaryAction}
|
||||||
@ -30,15 +31,15 @@ export const IssuedInvoiceDetailPage = () => {
|
|||||||
title={ctrl.state.pageError.title}
|
title={ctrl.state.pageError.title}
|
||||||
variant={ctrl.state.pageError.variant}
|
variant={ctrl.state.pageError.variant}
|
||||||
/>
|
/>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</div>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(ctrl.header && ctrl.document && ctrl.general)) {
|
if (!(ctrl.header && ctrl.document && ctrl.general)) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 sm:p-6">
|
<RdxPageFrame>
|
||||||
<div className="mx-auto w-full max-w-3xl">
|
<RdxPageContainer width="content">
|
||||||
<PageErrorState
|
<PageErrorState
|
||||||
description={t("pages.issued_invoices.detail.page_error.inconsistent.description")}
|
description={t("pages.issued_invoices.detail.page_error.inconsistent.description")}
|
||||||
secondaryAction={{
|
secondaryAction={{
|
||||||
@ -48,20 +49,23 @@ export const IssuedInvoiceDetailPage = () => {
|
|||||||
title={t("pages.issued_invoices.detail.page_error.unknown.title")}
|
title={t("pages.issued_invoices.detail.page_error.unknown.title")}
|
||||||
variant="unknown"
|
variant="unknown"
|
||||||
/>
|
/>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</div>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col overflow-hidden">
|
<RdxPageFrame
|
||||||
<IssuedInvoiceDetailHeader
|
contentClassName="lg:overflow-hidden"
|
||||||
actions={ctrl.actions}
|
header={
|
||||||
header={ctrl.header}
|
<IssuedInvoiceDetailHeader
|
||||||
onBack={ctrl.navigation.navigateBack}
|
actions={ctrl.actions}
|
||||||
onExecuteAction={ctrl.handlers.executeAction}
|
header={ctrl.header}
|
||||||
/>
|
onBack={ctrl.navigation.navigateBack}
|
||||||
|
onExecuteAction={ctrl.handlers.executeAction}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<IssuedInvoiceDetailContent
|
<IssuedInvoiceDetailContent
|
||||||
documentPane={
|
documentPane={
|
||||||
<IssuedInvoiceDocumentPane
|
<IssuedInvoiceDocumentPane
|
||||||
@ -80,6 +84,6 @@ export const IssuedInvoiceDetailPage = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||||
import { ToggleGroup, ToggleGroupItem } from "@repo/shadcn-ui/components";
|
import { ToggleGroup, ToggleGroupItem } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { RdxBadge } from "@repo/rdx-ui/components";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ErrorAlert, PageErrorState, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
import { ErrorAlert, PageErrorState, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||||
import { useReturnToNavigation } from "@erp/core/hooks";
|
import { useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { LogoVerifactu } from "@repo/rdx-ui/components";
|
import { LogoVerifactu, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||||
import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components";
|
import { Alert, AlertDescription, AlertTitle, Button } from "@repo/shadcn-ui/components";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@ -58,8 +58,8 @@ export const ListIssuedInvoicesPage = () => {
|
|||||||
|
|
||||||
if (hasBlockingError && pageError) {
|
if (hasBlockingError && pageError) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 sm:p-6">
|
<RdxPageFrame>
|
||||||
<div className="mx-auto w-full max-w-3xl">
|
<RdxPageContainer width="content">
|
||||||
<PageErrorState
|
<PageErrorState
|
||||||
description={pageError.description}
|
description={pageError.description}
|
||||||
primaryAction={
|
primaryAction={
|
||||||
@ -88,46 +88,46 @@ export const ListIssuedInvoicesPage = () => {
|
|||||||
title={pageError.title}
|
title={pageError.title}
|
||||||
variant={pageError.variant}
|
variant={pageError.variant}
|
||||||
/>
|
/>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</div>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<RdxPageFrame>
|
||||||
<PageHeader
|
<RdxPageContainer className="space-y-6" width="full">
|
||||||
description={t("pages.issued_invoices.list.description")}
|
<PageHeader
|
||||||
rightSlot={
|
description={t("pages.issued_invoices.list.description")}
|
||||||
<Button
|
rightSlot={
|
||||||
aria-label={t("pages.issued_invoices.create.title")}
|
<Button
|
||||||
className="hidden"
|
aria-label={t("pages.issued_invoices.create.title")}
|
||||||
onClick={() => navigate("/issued-invoices/create")}
|
className="hidden"
|
||||||
>
|
onClick={() => navigate("/issued-invoices/create")}
|
||||||
<PlusIcon aria-hidden className="mr-2 size-4" />
|
>
|
||||||
{t("pages.issued_invoices.create.title")}
|
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||||
</Button>
|
{t("pages.issued_invoices.create.title")}
|
||||||
}
|
</Button>
|
||||||
title={t("pages.issued_invoices.list.title")}
|
}
|
||||||
/>
|
title={t("pages.issued_invoices.list.title")}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="mx-auto w-full space-y-4">
|
<div className="mx-auto w-full space-y-4">
|
||||||
{hasRefreshError && refreshError ? (
|
{hasRefreshError && refreshError ? (
|
||||||
<ErrorAlert message={refreshError.message} title={refreshError.title} />
|
<ErrorAlert message={refreshError.message} title={refreshError.title} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Alert className="bg-green-50 text-green-800 flex items-center justify-between gap-6">
|
<Alert className="bg-green-50 text-green-800 flex items-center justify-between gap-6">
|
||||||
<div>
|
<div>
|
||||||
<AlertTitle className="font-semibold text-green-800">¡Atención!</AlertTitle>
|
<AlertTitle className="font-semibold text-green-800">¡Atención!</AlertTitle>
|
||||||
<AlertDescription className="text-green-800">
|
<AlertDescription className="text-green-800">
|
||||||
Las facturas de esta pantalla son de solo lectura. Se generan automáticamente al
|
Las facturas de esta pantalla son de solo lectura. Se generan automáticamente al
|
||||||
emitir una proforma aprobada y no se pueden modificar ni eliminar. Solo puedes
|
emitir una proforma aprobada y no se pueden modificar ni eliminar. Solo puedes
|
||||||
descargar el PDF de cada factura.
|
descargar el PDF de cada factura.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</div>
|
</div>
|
||||||
<LogoVerifactu className="text-green-800 w-50" color="" />
|
<LogoVerifactu className="text-green-800 w-50" color="" />
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-auto">
|
|
||||||
<IssuedInvoicesGrid
|
<IssuedInvoicesGrid
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={listCtrl.data}
|
data={listCtrl.data}
|
||||||
@ -164,10 +164,10 @@ export const ListIssuedInvoicesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{listCtrl.data.totalItems === 0 ? <IssuedInvoicesEmptyGrid /> : null}
|
{listCtrl.data.totalItems === 0 ? <IssuedInvoicesEmptyGrid /> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./issued-invoices-layout";
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
|
||||||
|
|
||||||
export const IssuedInvoicesLayout = ({ children }: PropsWithChildren) => {
|
|
||||||
return <div className="space-y-4">{children}</div>;
|
|
||||||
};
|
|
||||||
@ -38,7 +38,7 @@ export const ProformaCreateCustomerField = ({
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-10 w-full justify-between border bg-muted/50 px-3 font-normal text-left text-foreground hover:bg-muted/50",
|
"h-10 w-full justify-between border bg-muted/20 px-3 font-normal text-left text-foreground hover:bg-muted/50",
|
||||||
!selectedCustomer && "text-muted-foreground"
|
!selectedCustomer && "text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const ProformaCreateHeader = ({
|
|||||||
actions={
|
actions={
|
||||||
<FormActionsBar align="end" reverseOnMobile={false}>
|
<FormActionsBar align="end" reverseOnMobile={false}>
|
||||||
<PageKeyboardShortcutsButton
|
<PageKeyboardShortcutsButton
|
||||||
className="hidden sm:flex"
|
className="hidden"
|
||||||
label="Ver atajos de teclado"
|
label="Ver atajos de teclado"
|
||||||
shortcuts={[
|
shortcuts={[
|
||||||
{ keys: "Ctrl+S", label: "Crear y continuar" },
|
{ keys: "Ctrl+S", label: "Crear y continuar" },
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import type { CustomerSelectionOption } from "@erp/customers";
|
import type { CustomerSelectionOption } from "@erp/customers";
|
||||||
import {
|
import {
|
||||||
DatePickerField,
|
DatePickerField,
|
||||||
FormSectionCard,
|
RdxFormGrid,
|
||||||
FormSectionGrid,
|
RdxPanel,
|
||||||
SelectField,
|
SelectField,
|
||||||
type SelectFieldItem,
|
type SelectFieldItem,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
@ -43,13 +43,14 @@ export const ProformaCreateInitialInfoCard = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSectionCard
|
<RdxPanel
|
||||||
icon={<FileTextIcon className="size-5" />}
|
icon={<FileTextIcon className="size-5" />}
|
||||||
title={t("proformas.create.initial_info.title", "Información inicial")}
|
title={t("proformas.create.initial_info.title", "Información inicial")}
|
||||||
|
variant="card"
|
||||||
>
|
>
|
||||||
<FormSectionGrid className="md:grid-cols-6 2xl:grid-cols-12">
|
<RdxFormGrid className="md:grid-cols-6 2xl:grid-cols-12">
|
||||||
<ProformaCreateCustomerField
|
<ProformaCreateCustomerField
|
||||||
className="md:col-span-4"
|
className="md:col-span-8"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClearCustomer={onClearCustomer}
|
onClearCustomer={onClearCustomer}
|
||||||
onSelectCustomer={onSelectCustomer}
|
onSelectCustomer={onSelectCustomer}
|
||||||
@ -57,7 +58,7 @@ export const ProformaCreateInitialInfoCard = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DatePickerField<ProformaCreateForm>
|
<DatePickerField<ProformaCreateForm>
|
||||||
className="md:col-span-2"
|
className="md:col-span-4"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label={t("proformas.create.initial_info.proforma_date.label", "Fecha")}
|
label={t("proformas.create.initial_info.proforma_date.label", "Fecha")}
|
||||||
name="proformaDate"
|
name="proformaDate"
|
||||||
@ -69,7 +70,7 @@ export const ProformaCreateInitialInfoCard = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectField<ProformaCreateForm>
|
<SelectField<ProformaCreateForm>
|
||||||
className="md:col-span-2"
|
className="md:col-span-3"
|
||||||
disabled={disabled || isProformaSeriesLoading}
|
disabled={disabled || isProformaSeriesLoading}
|
||||||
items={proformaSeriesOptions}
|
items={proformaSeriesOptions}
|
||||||
label={t("proformas.create.initial_info.proforma_series.label", "Serie de esta proforma")}
|
label={t("proformas.create.initial_info.proforma_series.label", "Serie de esta proforma")}
|
||||||
@ -87,7 +88,7 @@ export const ProformaCreateInitialInfoCard = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectField<ProformaCreateForm>
|
<SelectField<ProformaCreateForm>
|
||||||
className="md:col-span-2"
|
className="md:col-span-3"
|
||||||
disabled={disabled || isTargetInvoiceSeriesLoading}
|
disabled={disabled || isTargetInvoiceSeriesLoading}
|
||||||
items={targetInvoiceSeriesOptions}
|
items={targetInvoiceSeriesOptions}
|
||||||
label={t("proformas.create.initial_info.invoice_series.label", "Serie de la factura")}
|
label={t("proformas.create.initial_info.invoice_series.label", "Serie de la factura")}
|
||||||
@ -100,7 +101,7 @@ export const ProformaCreateInitialInfoCard = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectField<ProformaCreateForm>
|
<SelectField<ProformaCreateForm>
|
||||||
className="md:col-span-4"
|
className="md:col-span-6"
|
||||||
clearable
|
clearable
|
||||||
clearLabel={t("form_fields.proformas.payment_term.clear_label", "Sin plazo de pago")}
|
clearLabel={t("form_fields.proformas.payment_term.clear_label", "Sin plazo de pago")}
|
||||||
disabled={disabled || isPaymentTermsLoading}
|
disabled={disabled || isPaymentTermsLoading}
|
||||||
@ -115,7 +116,7 @@ export const ProformaCreateInitialInfoCard = ({
|
|||||||
: t("form_fields.proformas.payment_term.placeholder", "Selecciona un plazo de pago")
|
: t("form_fields.proformas.payment_term.placeholder", "Selecciona un plazo de pago")
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FormSectionGrid>
|
</RdxFormGrid>
|
||||||
</FormSectionCard>
|
</RdxPanel>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
|
||||||
|
import { ProformaCreateSkeleton } from "./proforma-create-skeleton";
|
||||||
|
|
||||||
|
test("Proforma Create skeleton comparte el frame del editor productivo", () => {
|
||||||
|
const html = renderToStaticMarkup(<ProformaCreateSkeleton />);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
@ -1,16 +1,25 @@
|
|||||||
|
import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||||
import { Skeleton } from "@repo/shadcn-ui/components";
|
import { Skeleton } from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
export const ProformaCreateSkeleton = () => {
|
export const ProformaCreateSkeleton = () => {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
<RdxPageFrame
|
||||||
<div className="border-b px-6 py-4">
|
contentClassName="bg-muted/20"
|
||||||
<Skeleton className="h-10 w-full max-w-md" />
|
header={
|
||||||
</div>
|
<div className="border-b px-6 py-4">
|
||||||
<div className="flex-1 space-y-6 overflow-y-auto p-6">
|
<Skeleton className="h-10 w-full max-w-md" />
|
||||||
<Skeleton className="h-10 w-full" />
|
</div>
|
||||||
<Skeleton className="h-64 w-full" />
|
}
|
||||||
<Skeleton className="h-48 w-full" />
|
>
|
||||||
</div>
|
<RdxPageContainer width="content">
|
||||||
</div>
|
<RdxEditorLayout>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Skeleton className="h-10 w-full" />
|
||||||
|
<Skeleton className="h-64 w-full" />
|
||||||
|
<Skeleton className="h-48 w-full" />
|
||||||
|
</div>
|
||||||
|
</RdxEditorLayout>
|
||||||
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { ErrorAlert } from "@erp/core/components";
|
import { ErrorAlert } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { QuickCreateCustomerDialog, SelectCustomerDialog } from "@erp/customers";
|
import { QuickCreateCustomerDialog, SelectCustomerDialog } from "@erp/customers";
|
||||||
import { RdxEditorPageLayout, RdxPageContainer, RdxPageStack } from "@repo/rdx-ui/components";
|
import {
|
||||||
|
RdxEditorLayout,
|
||||||
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
|
RdxPageStack,
|
||||||
|
} from "@repo/rdx-ui/components";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
import { ProformaInfoAlert } from "../../../update/ui/components";
|
import { ProformaInfoAlert } from "../../../update/ui/components";
|
||||||
@ -16,26 +21,25 @@ export const ProformaCreatePage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RdxPageStack className="fixed inset-0 min-h-0 overflow-hidden" spacing="none">
|
<FormProvider {...createCtrl.form}>
|
||||||
<FormProvider {...createCtrl.form}>
|
<UnsavedChangesProvider isDirty={createCtrl.form.formState.isDirty}>
|
||||||
<UnsavedChangesProvider isDirty={createCtrl.form.formState.isDirty}>
|
<RdxPageFrame
|
||||||
<ProformaCreateHeader
|
contentClassName="bg-muted/20"
|
||||||
formId={createCtrl.formId}
|
header={
|
||||||
isSaving={createCtrl.isCreating}
|
<ProformaCreateHeader
|
||||||
onCancel={navigateBack}
|
formId={createCtrl.formId}
|
||||||
>
|
isSaving={createCtrl.isCreating}
|
||||||
<ProformaInfoAlert>
|
onCancel={navigateBack}
|
||||||
<span className="font-semibold">Importante: </span>Las proformas no tienen validez
|
>
|
||||||
fiscal. Puedes convertirlas en facturas cuando sean aceptadas por el cliente.
|
<ProformaInfoAlert>
|
||||||
</ProformaInfoAlert>
|
<span className="font-semibold">Importante: </span>Las proformas no tienen validez
|
||||||
</ProformaCreateHeader>
|
fiscal. Puedes convertirlas en facturas cuando sean aceptadas por el cliente.
|
||||||
|
</ProformaInfoAlert>
|
||||||
<RdxPageContainer
|
</ProformaCreateHeader>
|
||||||
className="min-h-0 flex-1 overflow-y-auto bg-muted/20"
|
}
|
||||||
padding="none"
|
>
|
||||||
width="full"
|
<RdxPageContainer width="content">
|
||||||
>
|
<RdxEditorLayout>
|
||||||
<RdxEditorPageLayout width="simple">
|
|
||||||
<RdxPageStack spacing="default">
|
<RdxPageStack spacing="default">
|
||||||
{createCtrl.isCreateError && (
|
{createCtrl.isCreateError && (
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
@ -75,16 +79,16 @@ export const ProformaCreatePage = () => {
|
|||||||
targetInvoiceSeriesOptions={createCtrl.targetInvoiceSeriesOptions}
|
targetInvoiceSeriesOptions={createCtrl.targetInvoiceSeriesOptions}
|
||||||
/>
|
/>
|
||||||
</RdxPageStack>
|
</RdxPageStack>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
|
|
||||||
<SelectCustomerDialog
|
<SelectCustomerDialog
|
||||||
ctrl={selectCustomerCtrl.selectCtrl}
|
ctrl={selectCustomerCtrl.selectCtrl}
|
||||||
onCreateNewCustomerClick={selectCustomerCtrl.openQuickCreateFromSelection}
|
onCreateNewCustomerClick={selectCustomerCtrl.openQuickCreateFromSelection}
|
||||||
/>
|
/>
|
||||||
<QuickCreateCustomerDialog ctrl={selectCustomerCtrl.createCtrl} />
|
<QuickCreateCustomerDialog ctrl={selectCustomerCtrl.createCtrl} />
|
||||||
</UnsavedChangesProvider>
|
</UnsavedChangesProvider>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</RdxPageStack>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -18,10 +18,10 @@ import { useCreateRecurringProformaFromProforma } from "../../../recurring-profo
|
|||||||
import { buildRecurringProformaDetailPath } from "../../../recurring-proformas/shared/recurring-proforma-navigation";
|
import { buildRecurringProformaDetailPath } from "../../../recurring-proformas/shared/recurring-proforma-navigation";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildCreateRecurringProformaFromProformaDefaultValues,
|
|
||||||
buildCreateRecurringProformaFromProformaInput,
|
|
||||||
type CreateRecurringProformaFromProformaForm,
|
type CreateRecurringProformaFromProformaForm,
|
||||||
CreateRecurringProformaFromProformaFormSchema,
|
CreateRecurringProformaFromProformaFormSchema,
|
||||||
|
buildCreateRecurringProformaFromProformaDefaultValues,
|
||||||
|
buildCreateRecurringProformaFromProformaInput,
|
||||||
} from "./use-create-recurring-proforma-from-proforma-dialog-controller.utils";
|
} from "./use-create-recurring-proforma-from-proforma-dialog-controller.utils";
|
||||||
|
|
||||||
const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection => {
|
const normalizeSubmitError = (error: unknown): Error | ValidationErrorCollection => {
|
||||||
@ -41,6 +41,9 @@ export interface CreateRecurringProformaFromProformaDialogControllerParams {
|
|||||||
proformaReference: string | null;
|
proformaReference: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateRecurringProformaFromProformaTarget =
|
||||||
|
CreateRecurringProformaFromProformaDialogControllerParams;
|
||||||
|
|
||||||
export const useCreateRecurringProformaFromProformaDialogController = ({
|
export const useCreateRecurringProformaFromProformaDialogController = ({
|
||||||
proformaId,
|
proformaId,
|
||||||
proformaReference,
|
proformaReference,
|
||||||
@ -51,6 +54,10 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
|
|||||||
const mutation = useCreateRecurringProformaFromProforma();
|
const mutation = useCreateRecurringProformaFromProforma();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const submitInProgressRef = useRef(false);
|
const submitInProgressRef = useRef(false);
|
||||||
|
const targetRef = useRef<CreateRecurringProformaFromProformaTarget>({
|
||||||
|
proformaId,
|
||||||
|
proformaReference,
|
||||||
|
});
|
||||||
const form = useHookForm<CreateRecurringProformaFromProformaForm>({
|
const form = useHookForm<CreateRecurringProformaFromProformaForm>({
|
||||||
resolverSchema: CreateRecurringProformaFromProformaFormSchema,
|
resolverSchema: CreateRecurringProformaFromProformaFormSchema,
|
||||||
initialValues: buildCreateRecurringProformaFromProformaDefaultValues(proformaReference),
|
initialValues: buildCreateRecurringProformaFromProformaDefaultValues(proformaReference),
|
||||||
@ -77,8 +84,10 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDialog = () => {
|
const openDialog = (target?: CreateRecurringProformaFromProformaTarget) => {
|
||||||
form.reset(buildCreateRecurringProformaFromProformaDefaultValues(proformaReference));
|
const nextTarget = target ?? { proformaId, proformaReference };
|
||||||
|
targetRef.current = nextTarget;
|
||||||
|
form.reset(buildCreateRecurringProformaFromProformaDefaultValues(nextTarget.proformaReference));
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -100,7 +109,7 @@ export const useCreateRecurringProformaFromProformaDialogController = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const created = await mutation.mutateAsync({
|
const created = await mutation.mutateAsync({
|
||||||
proformaId,
|
proformaId: targetRef.current.proformaId,
|
||||||
data: buildCreateRecurringProformaFromProformaInput(values),
|
data: buildCreateRecurringProformaFromProformaInput(values),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ type ProformaDetailContentProps = {
|
|||||||
|
|
||||||
export const ProformaDetailContent = ({ documentPane, inspector }: ProformaDetailContentProps) => {
|
export const ProformaDetailContent = ({ documentPane, inspector }: ProformaDetailContentProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-0 flex-1 overflow-auto lg:overflow-hidden ">
|
<div className="min-h-full lg:h-full lg:min-h-0 lg:overflow-hidden">
|
||||||
<div className="grid min-h-full grid-cols-1 lg:h-full lg:min-h-0 lg:overflow-hidden lg:grid-cols-[minmax(0,1fr)_340px] xl:grid-cols-[minmax(0,1fr)_380px]">
|
<div className="grid min-h-full grid-cols-1 lg:h-full lg:min-h-0 lg:overflow-hidden lg:grid-cols-[minmax(0,1fr)_340px] xl:grid-cols-[minmax(0,1fr)_380px]">
|
||||||
{documentPane}
|
{documentPane}
|
||||||
{inspector}
|
{inspector}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { PageErrorState } from "@erp/core/components";
|
import { PageErrorState } from "@erp/core/components";
|
||||||
import { RdxPageStack } from "@repo/rdx-ui/components";
|
import { 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";
|
||||||
@ -41,14 +41,17 @@ export const ProformaDetailPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RdxPageStack className="fixed inset-0 min-h-0 overflow-hidden" spacing="none">
|
<RdxPageFrame
|
||||||
<ProformaDetailHeader
|
contentClassName="lg:overflow-hidden"
|
||||||
actions={ctrl.actions}
|
header={
|
||||||
header={ctrl.header}
|
<ProformaDetailHeader
|
||||||
onBack={ctrl.navigation.navigateBack}
|
actions={ctrl.actions}
|
||||||
onExecuteAction={ctrl.handlers.executeAction}
|
header={ctrl.header}
|
||||||
/>
|
onBack={ctrl.navigation.navigateBack}
|
||||||
|
onExecuteAction={ctrl.handlers.executeAction}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<ProformaDetailContent
|
<ProformaDetailContent
|
||||||
documentPane={
|
documentPane={
|
||||||
<ProformaDocumentPane
|
<ProformaDocumentPane
|
||||||
@ -114,6 +117,6 @@ export const ProformaDetailPage = () => {
|
|||||||
open={ctrl.dialogs.createRecurring.open}
|
open={ctrl.dialogs.createRecurring.open}
|
||||||
unit={ctrl.dialogs.createRecurring.unit}
|
unit={ctrl.dialogs.createRecurring.unit}
|
||||||
/>
|
/>
|
||||||
</RdxPageStack>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
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 { 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 type { ProformaListRow } from "../../../../shared";
|
||||||
import { PROFORMA_STATUS } from "../../../../shared";
|
import { PROFORMA_STATUS } from "../../../../shared";
|
||||||
|
|
||||||
import { useProformasGridColumns } from "./use-proforma-grid-columns";
|
import { useProformasGridColumns } from "./use-proforma-grid-columns";
|
||||||
|
|
||||||
const proformaFixture: ProformaListRow = {
|
const proformaFixture: ProformaListRow = {
|
||||||
@ -104,12 +105,16 @@ test("mantiene las destructivas en overflow y conserva disabled", () => {
|
|||||||
assert.equal(viewAction?.disabled, true);
|
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 didView = false;
|
||||||
let didEdit = false;
|
let didEdit = false;
|
||||||
|
let didCreateRecurring = false;
|
||||||
let didDelete = false;
|
let didDelete = false;
|
||||||
|
|
||||||
const props = getRowActionsProps("normal", {
|
const props = getRowActionsProps("normal", {
|
||||||
|
onCreateRecurringClick: () => {
|
||||||
|
didCreateRecurring = true;
|
||||||
|
},
|
||||||
onDeleteClick: () => {
|
onDeleteClick: () => {
|
||||||
didDelete = true;
|
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 === "view")?.onSelect();
|
||||||
props.primaryActions?.find((action) => action.id === "edit")?.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();
|
props.overflowActions?.find((action) => action.id === "delete")?.onSelect();
|
||||||
|
|
||||||
assert.equal(didView, true);
|
assert.equal(didView, true);
|
||||||
assert.equal(didEdit, true);
|
assert.equal(didEdit, true);
|
||||||
|
assert.equal(didCreateRecurring, true);
|
||||||
assert.equal(didDelete, 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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
import { EmptyCellValue } from "@erp/core/components";
|
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 { DateHelper } from "@repo/rdx-utils";
|
||||||
import {
|
import { Checkbox, Spinner } from "@repo/shadcn-ui/components";
|
||||||
Checkbox,
|
|
||||||
Spinner,
|
|
||||||
} from "@repo/shadcn-ui/components";
|
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
|
CalendarSyncIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
@ -37,6 +35,7 @@ type GridActionHandlers = {
|
|||||||
onIssueClick?: (proforma: ProformaListRow) => void;
|
onIssueClick?: (proforma: ProformaListRow) => void;
|
||||||
onArchiveClick?: (proforma: ProformaListRow) => void;
|
onArchiveClick?: (proforma: ProformaListRow) => void;
|
||||||
onDuplicateClick?: (proforma: ProformaListRow) => void;
|
onDuplicateClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onCreateRecurringClick?: (proforma: ProformaListRow) => void;
|
||||||
onUnarchiveClick?: (proforma: ProformaListRow) => void;
|
onUnarchiveClick?: (proforma: ProformaListRow) => void;
|
||||||
onDeleteClick?: (proforma: ProformaListRow) => void;
|
onDeleteClick?: (proforma: ProformaListRow) => void;
|
||||||
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void;
|
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void;
|
||||||
@ -52,10 +51,8 @@ type GridActionHandlers = {
|
|||||||
isPdfDownloading?: boolean;
|
isPdfDownloading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderActionIcon = (
|
const renderActionIcon = (Icon: ComponentType<{ className?: string }>, loading?: boolean) =>
|
||||||
Icon: ComponentType<{ className?: string }>,
|
loading ? <Spinner className="size-4" /> : <Icon className="size-4" />;
|
||||||
loading?: boolean
|
|
||||||
) => (loading ? <Spinner className="size-4" /> : <Icon className="size-4" />);
|
|
||||||
|
|
||||||
export function useProformasGridColumns(
|
export function useProformasGridColumns(
|
||||||
scope: ProformaListScope,
|
scope: ProformaListScope,
|
||||||
@ -312,6 +309,7 @@ export function useProformasGridColumns(
|
|||||||
const handleView = () => actionHandlers.onViewClick?.(proforma);
|
const handleView = () => actionHandlers.onViewClick?.(proforma);
|
||||||
const handleEdit = () => actionHandlers.onEditClick?.(proforma);
|
const handleEdit = () => actionHandlers.onEditClick?.(proforma);
|
||||||
const handleDuplicate = () => actionHandlers.onDuplicateClick?.(proforma);
|
const handleDuplicate = () => actionHandlers.onDuplicateClick?.(proforma);
|
||||||
|
const handleCreateRecurring = () => actionHandlers.onCreateRecurringClick?.(proforma);
|
||||||
const handleArchive = () => actionHandlers.onArchiveClick?.(proforma);
|
const handleArchive = () => actionHandlers.onArchiveClick?.(proforma);
|
||||||
const handleUnarchive = () => actionHandlers.onUnarchiveClick?.(proforma);
|
const handleUnarchive = () => actionHandlers.onUnarchiveClick?.(proforma);
|
||||||
const handleIssue = () => actionHandlers.onIssueClick?.(proforma);
|
const handleIssue = () => actionHandlers.onIssueClick?.(proforma);
|
||||||
@ -416,6 +414,14 @@ export function useProformasGridColumns(
|
|||||||
: undefined,
|
: undefined,
|
||||||
onSelect: handleDuplicate,
|
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",
|
id: "archive",
|
||||||
label: t("pages.proformas.list.actions.archive"),
|
label: t("pages.proformas.list.actions.archive"),
|
||||||
|
|||||||
@ -32,6 +32,8 @@ import { ChangeProformaStatusDialog } from "../../../change-status";
|
|||||||
import { prepareChangeProformaStatusTargets } from "../../../change-status/utils";
|
import { prepareChangeProformaStatusTargets } from "../../../change-status/utils";
|
||||||
import { DeleteProformaDialog } from "../../../delete";
|
import { DeleteProformaDialog } from "../../../delete";
|
||||||
import { prepareDeleteProformaTargets } from "../../../delete/utils";
|
import { prepareDeleteProformaTargets } from "../../../delete/utils";
|
||||||
|
import { useCreateRecurringProformaFromProformaDialogController } from "../../../detail/controllers";
|
||||||
|
import { CreateRecurringProformaFromProformaDialog } from "../../../detail/ui/components";
|
||||||
import { downloadProformaPDFApi } from "../../../download-pdf/api";
|
import { downloadProformaPDFApi } from "../../../download-pdf/api";
|
||||||
import { IssueProformaDialog } from "../../../issue-proforma";
|
import { IssueProformaDialog } from "../../../issue-proforma";
|
||||||
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
|
import { prepareIssueProformaTarget } from "../../../issue-proforma/utils";
|
||||||
@ -238,6 +240,10 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
|||||||
goHome,
|
goHome,
|
||||||
isRetrying,
|
isRetrying,
|
||||||
} = useListProformasPageController(scope);
|
} = useListProformasPageController(scope);
|
||||||
|
const createRecurringDialogCtrl = useCreateRecurringProformaFromProformaDialogController({
|
||||||
|
proformaId: "",
|
||||||
|
proformaReference: null,
|
||||||
|
});
|
||||||
|
|
||||||
const handleEditClick = (proformaId: string) => {
|
const handleEditClick = (proformaId: string) => {
|
||||||
navigate({
|
navigate({
|
||||||
@ -293,6 +299,11 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
|||||||
onViewClick: (proforma) => handleOpenDetailClick(proforma.id),
|
onViewClick: (proforma) => handleOpenDetailClick(proforma.id),
|
||||||
onEditClick: (proforma) => handleEditClick(proforma.id),
|
onEditClick: (proforma) => handleEditClick(proforma.id),
|
||||||
onDuplicateClick: handleDuplicateClick,
|
onDuplicateClick: handleDuplicateClick,
|
||||||
|
onCreateRecurringClick: (proforma) =>
|
||||||
|
createRecurringDialogCtrl.openDialog({
|
||||||
|
proformaId: proforma.id,
|
||||||
|
proformaReference: proforma.proformaReference,
|
||||||
|
}),
|
||||||
onIssueClick: (proformaRow) =>
|
onIssueClick: (proformaRow) =>
|
||||||
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
|
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
|
||||||
onArchiveClick: archiveDialogCtrl.openDialog,
|
onArchiveClick: archiveDialogCtrl.openDialog,
|
||||||
@ -550,6 +561,21 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
|||||||
targets={changeStatusDialogCtrl.targets}
|
targets={changeStatusDialogCtrl.targets}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CreateRecurringProformaFromProformaDialog
|
||||||
|
form={createRecurringDialogCtrl.form}
|
||||||
|
formId={createRecurringDialogCtrl.formId}
|
||||||
|
isSubmitting={createRecurringDialogCtrl.isSubmitting}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
createRecurringDialogCtrl.closeDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSubmit={createRecurringDialogCtrl.onSubmit}
|
||||||
|
onUnitChange={createRecurringDialogCtrl.changeUnit}
|
||||||
|
open={createRecurringDialogCtrl.open}
|
||||||
|
unit={createRecurringDialogCtrl.unit}
|
||||||
|
/>
|
||||||
|
|
||||||
<DeleteProformaDialog
|
<DeleteProformaDialog
|
||||||
isSubmitting={deleteDialogCtrl.isSubmitting}
|
isSubmitting={deleteDialogCtrl.isSubmitting}
|
||||||
onConfirm={deleteDialogCtrl.confirmDelete}
|
onConfirm={deleteDialogCtrl.confirmDelete}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { ErrorAlert } from "@erp/core/components";
|
|||||||
import {
|
import {
|
||||||
RdxPageActions,
|
RdxPageActions,
|
||||||
RdxPageContainer,
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
RdxPageHeader,
|
RdxPageHeader,
|
||||||
RdxPageStack,
|
RdxPageStack,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
@ -30,22 +31,24 @@ export const ScopedListProformasPageView = ({
|
|||||||
dialogs,
|
dialogs,
|
||||||
}: ScopedListProformasPageViewProps) => {
|
}: ScopedListProformasPageViewProps) => {
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<RdxPageStack spacing="default">
|
<RdxPageContainer width="full">
|
||||||
<RdxPageHeader
|
<RdxPageStack spacing="default">
|
||||||
actions={primaryAction ? <RdxPageActions primary={primaryAction} /> : undefined}
|
<RdxPageHeader
|
||||||
description={description}
|
actions={primaryAction ? <RdxPageActions primary={primaryAction} /> : undefined}
|
||||||
title={title}
|
description={description}
|
||||||
/>
|
title={title}
|
||||||
|
/>
|
||||||
|
|
||||||
{refreshError ? (
|
{refreshError ? (
|
||||||
<ErrorAlert message={refreshError.message} title={refreshError.title} />
|
<ErrorAlert message={refreshError.message} title={refreshError.title} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{content}
|
{content}
|
||||||
</RdxPageStack>
|
</RdxPageStack>
|
||||||
|
|
||||||
{dialogs}
|
{dialogs}
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./proforma-layout";
|
|
||||||
export * from "./proforma-issuance-readiness-alert";
|
export * from "./proforma-issuance-readiness-alert";
|
||||||
export * from "./proforma-page-header";
|
export * from "./proforma-page-header";
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
|
||||||
|
|
||||||
export function ProformaLayout({ children }: PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-full w-full" id="proformas ">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { RdxPageHeader } from "@repo/rdx-ui/components";
|
import { RdxEditorHeader } from "@repo/rdx-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
@ -26,18 +26,17 @@ export const ProformaPageHeader = ({
|
|||||||
contentClassName,
|
contentClassName,
|
||||||
}: ProformaPageHeaderProps) => {
|
}: ProformaPageHeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<RdxPageHeader
|
<RdxEditorHeader
|
||||||
actions={actions}
|
actions={actions}
|
||||||
backLabel={backLabel}
|
backLabel={backLabel}
|
||||||
className={cn("shrink-0", className)}
|
className={cn("shrink-0", className)}
|
||||||
contentClassName={contentClassName}
|
contentClassName={contentClassName}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
showStatus={showStatus}
|
showStatus={showStatus}
|
||||||
statusLabel={statusLabel}
|
status={statusLabel}
|
||||||
sticky
|
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</RdxPageHeader>
|
</RdxEditorHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { CustomerSelectionOption } from "@erp/customers";
|
import type { CustomerSelectionOption } from "@erp/customers";
|
||||||
import { RdxEditorPageLayout, type SelectFieldItem } from "@repo/rdx-ui/components";
|
import { RdxEditorLayout, type SelectFieldItem } from "@repo/rdx-ui/components";
|
||||||
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
import { preventEnterKeySubmitForm } from "@repo/rdx-ui/helpers";
|
||||||
import type { SubmitEventHandler } from "react";
|
import type { SubmitEventHandler } from "react";
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
className,
|
className,
|
||||||
}: ProformaUpdateEditorProps) => {
|
}: ProformaUpdateEditorProps) => {
|
||||||
return (
|
return (
|
||||||
<RdxEditorPageLayout
|
<RdxEditorLayout
|
||||||
aside={
|
aside={
|
||||||
<div className="flex min-w-0 flex-col gap-6">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
<ProformaUpdateRecipientEditor
|
<ProformaUpdateRecipientEditor
|
||||||
@ -75,7 +75,6 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className={className}
|
className={className}
|
||||||
width="full"
|
|
||||||
>
|
>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}>
|
<form id={formId} noValidate onKeyDown={preventEnterKeySubmitForm} onSubmit={onSubmit}>
|
||||||
@ -91,6 +90,6 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -52,6 +52,21 @@ test("conecta la alta rápida directa y la alta rápida desde selector con callb
|
|||||||
PercentageField: () => null,
|
PercentageField: () => null,
|
||||||
RdxEditorStickyFooter: () => null,
|
RdxEditorStickyFooter: () => null,
|
||||||
RdxPageContainer: ({ children }: { children: ReactNode }) => children,
|
RdxPageContainer: ({ children }: { children: ReactNode }) => children,
|
||||||
|
RdxPageFrame: ({
|
||||||
|
children,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
header?: ReactNode;
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
{header}
|
||||||
|
{children}
|
||||||
|
{footer}
|
||||||
|
</>
|
||||||
|
),
|
||||||
RdxPageStack: ({ children }: { children: ReactNode }) => children,
|
RdxPageStack: ({ children }: { children: ReactNode }) => children,
|
||||||
RhfSubmitActionButton: () => null,
|
RhfSubmitActionButton: () => null,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
RdxBadge,
|
RdxBadge,
|
||||||
RdxEditorActionBar,
|
RdxEditorActionBar,
|
||||||
RdxPageContainer,
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
RdxPageStack,
|
RdxPageStack,
|
||||||
RhfSubmitActionButton,
|
RhfSubmitActionButton,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
@ -94,178 +95,174 @@ export const ProformaUpdatePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasPageAlerts = updateCtrl.isUpdateError || updateCtrl.isSeriesLoadError;
|
const hasPageAlerts = updateCtrl.isUpdateError || updateCtrl.isSeriesLoadError;
|
||||||
|
const actionBar = (
|
||||||
|
<RdxEditorActionBar
|
||||||
|
leading={
|
||||||
|
<ProformaCompactTotals
|
||||||
|
currency={updateCtrl.currencyCode}
|
||||||
|
globalDiscountField={
|
||||||
|
<PercentageField
|
||||||
|
className="md:col-span-4 md:col-start-1"
|
||||||
|
disabled={updateCtrl.isUpdating}
|
||||||
|
inputClassName="bg-background"
|
||||||
|
label={t("proformas.update.totals.globalDiscountPercentage", "Descuento global")}
|
||||||
|
name="globalDiscountPercentage"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
showRec={updateCtrl.taxCtrl.hasRecPercentage}
|
||||||
|
showRetention={updateCtrl.taxCtrl.hasRetentionPercentage}
|
||||||
|
totals={updateCtrl.totalsCtrl.totals}
|
||||||
|
variant="embedded"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
primary={
|
||||||
|
<RhfSubmitActionButton
|
||||||
|
busyLabel="Guardando..."
|
||||||
|
disabled={updateCtrl.isUpdating}
|
||||||
|
formId={updateCtrl.formId}
|
||||||
|
isBusy={updateCtrl.form.formState.isSubmitting}
|
||||||
|
label="Guardar"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
secondary={
|
||||||
|
<CancelActionButton
|
||||||
|
disabled={updateCtrl.isUpdating}
|
||||||
|
label="Cancelar"
|
||||||
|
onCancel={handleCancel}
|
||||||
|
variant="outline"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RdxPageStack className="fixed inset-0 min-h-0 overflow-hidden" spacing="none">
|
<FormProvider {...updateCtrl.form}>
|
||||||
<FormProvider {...updateCtrl.form}>
|
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
||||||
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
<RdxPageFrame
|
||||||
<ProformaUpdateHeader
|
contentClassName="bg-muted/20"
|
||||||
formId={updateCtrl.formId}
|
footer={actionBar}
|
||||||
hasChanges={updateCtrl.form.formState.isDirty}
|
header={
|
||||||
helpAction={
|
<ProformaUpdateHeader
|
||||||
<ProformaUpdateHelpPopover
|
formId={updateCtrl.formId}
|
||||||
isDirty={updateCtrl.form.formState.isDirty}
|
hasChanges={updateCtrl.form.formState.isDirty}
|
||||||
isReadinessLoading={ctrl.isIssuanceReadinessLoading}
|
helpAction={
|
||||||
onRetryReadiness={handleRetryIssuanceReadiness}
|
<ProformaUpdateHelpPopover
|
||||||
readiness={ctrl.issuanceReadiness}
|
isDirty={updateCtrl.form.formState.isDirty}
|
||||||
readinessError={ctrl.issuanceReadinessError}
|
isReadinessLoading={ctrl.isIssuanceReadinessLoading}
|
||||||
/>
|
onRetryReadiness={handleRetryIssuanceReadiness}
|
||||||
}
|
readiness={ctrl.issuanceReadiness}
|
||||||
isSaving={updateCtrl.form.formState.isSubmitting}
|
readinessError={ctrl.issuanceReadinessError}
|
||||||
labels={{
|
|
||||||
title: proforma ? (
|
|
||||||
<span className="inline-flex max-w-full items-center gap-3">
|
|
||||||
<span>{t("pages.proformas.update.title", "Editar proforma")}</span>
|
|
||||||
<RdxBadge className="border-border/80 bg-muted text-muted-foreground">
|
|
||||||
{proforma.proformaReference}
|
|
||||||
</RdxBadge>
|
|
||||||
<ProformaStatusBadge status={proforma.status} />
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
t("pages.proformas.update.title", "Editar proforma")
|
|
||||||
),
|
|
||||||
back: "Volver",
|
|
||||||
modified: "Modificada",
|
|
||||||
cancel: "Cancelar",
|
|
||||||
save: "Guardar",
|
|
||||||
saving: "Guardando...",
|
|
||||||
moreActions: "Más acciones",
|
|
||||||
keyboardShortcuts: "Ver atajos de teclado",
|
|
||||||
duplicate: "Duplicar proforma",
|
|
||||||
exportPdf: "Exportar PDF",
|
|
||||||
delete: "Eliminar",
|
|
||||||
}}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
//onDelete={openDeleteDialog}
|
|
||||||
//onDuplicate={handleDuplicate}
|
|
||||||
//onExportPdf={handleExportPdf}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex min-h-0 flex-1 flex-col bg-muted/20">
|
|
||||||
<RdxPageContainer
|
|
||||||
className="min-h-0 flex-1 overflow-y-auto"
|
|
||||||
padding="default"
|
|
||||||
width="full"
|
|
||||||
>
|
|
||||||
{hasPageAlerts ? (
|
|
||||||
<RdxPageContainer className="pb-0" width="full">
|
|
||||||
<RdxPageStack spacing="compact">
|
|
||||||
{updateCtrl.isUpdateError && (
|
|
||||||
<ErrorAlert
|
|
||||||
message={
|
|
||||||
(updateCtrl.updateError as Error)?.message ??
|
|
||||||
t(
|
|
||||||
"pages.proformas.update.error_msg",
|
|
||||||
"Revisa los datos e inténtalo de nuevo."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
title={t(
|
|
||||||
"pages.proformas.update.error_title",
|
|
||||||
"No se pudo guardar los cambios"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{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(
|
|
||||||
"pages.proformas.update.series_load_error.title",
|
|
||||||
"No se pudieron cargar las series de facturación"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</RdxPageStack>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<RdxEditorActionBar
|
|
||||||
leading={
|
|
||||||
<ProformaCompactTotals
|
|
||||||
currency={updateCtrl.currencyCode}
|
|
||||||
globalDiscountField={
|
|
||||||
<PercentageField
|
|
||||||
className="md:col-span-4 md:col-start-1"
|
|
||||||
disabled={updateCtrl.isUpdating}
|
|
||||||
inputClassName="bg-background"
|
|
||||||
label={t(
|
|
||||||
"proformas.update.totals.globalDiscountPercentage",
|
|
||||||
"Descuento global"
|
|
||||||
)}
|
|
||||||
name="globalDiscountPercentage"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
showRec={updateCtrl.taxCtrl.hasRecPercentage}
|
|
||||||
showRetention={updateCtrl.taxCtrl.hasRetentionPercentage}
|
|
||||||
totals={updateCtrl.totalsCtrl.totals}
|
|
||||||
variant="embedded"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
primary={
|
|
||||||
<RhfSubmitActionButton
|
|
||||||
busyLabel="Guardando..."
|
|
||||||
disabled={updateCtrl.isUpdating}
|
|
||||||
formId={updateCtrl.formId}
|
|
||||||
isBusy={updateCtrl.form.formState.isSubmitting}
|
|
||||||
label="Guardar"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
secondary={
|
|
||||||
<CancelActionButton
|
|
||||||
disabled={updateCtrl.isUpdating}
|
|
||||||
label="Cancelar"
|
|
||||||
onCancel={handleCancel}
|
|
||||||
variant="outline"
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
isSaving={updateCtrl.form.formState.isSubmitting}
|
||||||
|
labels={{
|
||||||
|
title: proforma ? (
|
||||||
|
<span className="inline-flex max-w-full items-center gap-3">
|
||||||
|
<span>{t("pages.proformas.update.title", "Editar proforma")}</span>
|
||||||
|
<RdxBadge className="border-border/80 bg-muted text-muted-foreground">
|
||||||
|
{proforma.proformaReference}
|
||||||
|
</RdxBadge>
|
||||||
|
<ProformaStatusBadge status={proforma.status} />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
t("pages.proformas.update.title", "Editar proforma")
|
||||||
|
),
|
||||||
|
back: "Volver",
|
||||||
|
modified: "Modificada",
|
||||||
|
cancel: "Cancelar",
|
||||||
|
save: "Guardar",
|
||||||
|
saving: "Guardando...",
|
||||||
|
moreActions: "Más acciones",
|
||||||
|
keyboardShortcuts: "Ver atajos de teclado",
|
||||||
|
duplicate: "Duplicar proforma",
|
||||||
|
exportPdf: "Exportar PDF",
|
||||||
|
delete: "Eliminar",
|
||||||
|
}}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
//onDelete={openDeleteDialog}
|
||||||
|
//onDuplicate={handleDuplicate}
|
||||||
|
//onExportPdf={handleExportPdf}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
|
<RdxPageContainer padding="default" width="full">
|
||||||
|
{hasPageAlerts ? (
|
||||||
|
<RdxPageContainer className="pb-0" width="full">
|
||||||
|
<RdxPageStack spacing="compact">
|
||||||
|
{updateCtrl.isUpdateError && (
|
||||||
|
<ErrorAlert
|
||||||
|
message={
|
||||||
|
(updateCtrl.updateError as Error)?.message ??
|
||||||
|
t(
|
||||||
|
"pages.proformas.update.error_msg",
|
||||||
|
"Revisa los datos e inténtalo de nuevo."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title={t(
|
||||||
|
"pages.proformas.update.error_title",
|
||||||
|
"No se pudo guardar los cambios"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<SelectCustomerDialog
|
{updateCtrl.isSeriesLoadError && (
|
||||||
ctrl={selectCustomerCtrl.selectCtrl}
|
<ErrorAlert
|
||||||
onCreateNewCustomerClick={selectCustomerCtrl.openQuickCreateFromSelection}
|
message={
|
||||||
/>
|
updateCtrl.seriesLoadError instanceof Error
|
||||||
<QuickCreateCustomerDialog ctrl={selectCustomerCtrl.createCtrl} />
|
? updateCtrl.seriesLoadError.message
|
||||||
<ProformaUpdateCustomerChangeDialog
|
: t(
|
||||||
evaluation={customerChangeCtrl.evaluation}
|
"pages.proformas.update.series_load_error.message",
|
||||||
isSubmitting={customerChangeCtrl.isApplying}
|
"Inténtalo de nuevo más tarde."
|
||||||
loadError={customerChangeCtrl.loadError as Error | null}
|
)
|
||||||
onConfirm={customerChangeCtrl.confirmChange}
|
}
|
||||||
onOpenChange={(open) => {
|
title={t(
|
||||||
if (!open) {
|
"pages.proformas.update.series_load_error.title",
|
||||||
customerChangeCtrl.closeDialog();
|
"No se pudieron cargar las series de facturación"
|
||||||
}
|
)}
|
||||||
}}
|
/>
|
||||||
open={customerChangeCtrl.open}
|
)}
|
||||||
status={customerChangeCtrl.candidateStatus}
|
</RdxPageStack>
|
||||||
/>
|
</RdxPageContainer>
|
||||||
</UnsavedChangesProvider>
|
) : null}
|
||||||
</FormProvider>
|
|
||||||
</RdxPageStack>
|
<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>
|
||||||
|
</RdxPageFrame>
|
||||||
|
|
||||||
|
<SelectCustomerDialog
|
||||||
|
ctrl={selectCustomerCtrl.selectCtrl}
|
||||||
|
onCreateNewCustomerClick={selectCustomerCtrl.openQuickCreateFromSelection}
|
||||||
|
/>
|
||||||
|
<QuickCreateCustomerDialog ctrl={selectCustomerCtrl.createCtrl} />
|
||||||
|
<ProformaUpdateCustomerChangeDialog
|
||||||
|
evaluation={customerChangeCtrl.evaluation}
|
||||||
|
isSubmitting={customerChangeCtrl.isApplying}
|
||||||
|
loadError={customerChangeCtrl.loadError as Error | null}
|
||||||
|
onConfirm={customerChangeCtrl.confirmChange}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
customerChangeCtrl.closeDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
open={customerChangeCtrl.open}
|
||||||
|
status={customerChangeCtrl.candidateStatus}
|
||||||
|
/>
|
||||||
|
</UnsavedChangesProvider>
|
||||||
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import { PageFormHeader } from "@erp/core/components";
|
|
||||||
import { useHookForm } from "@erp/core/hooks";
|
import { useHookForm } from "@erp/core/hooks";
|
||||||
import { RdxEditorPageLayout } from "@repo/rdx-ui/components";
|
import {
|
||||||
|
RdxEditorHeader,
|
||||||
|
RdxEditorLayout,
|
||||||
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
|
} from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@ -50,35 +54,39 @@ export const RecurringProformaCreatePage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
<RdxPageFrame
|
||||||
<PageFormHeader
|
contentClassName="bg-muted/20"
|
||||||
actions={
|
header={
|
||||||
<div className="flex items-center gap-3">
|
<RdxEditorHeader
|
||||||
<Button
|
actions={
|
||||||
disabled={mutation.isPending}
|
<div className="flex items-center gap-3">
|
||||||
onClick={() => navigate("/recurring-proformas")}
|
<Button
|
||||||
type="button"
|
disabled={mutation.isPending}
|
||||||
variant="outline"
|
onClick={() => navigate("/recurring-proformas")}
|
||||||
>
|
type="button"
|
||||||
{t("recurring_proformas.create.cancel")}
|
variant="outline"
|
||||||
</Button>
|
>
|
||||||
<Button
|
{t("recurring_proformas.create.cancel")}
|
||||||
disabled={mutation.isPending}
|
</Button>
|
||||||
form={RECURRING_PROFORMA_CREATE_FORM_ID}
|
<Button
|
||||||
type="submit"
|
disabled={mutation.isPending}
|
||||||
>
|
form={RECURRING_PROFORMA_CREATE_FORM_ID}
|
||||||
{mutation.isPending
|
type="submit"
|
||||||
? t("recurring_proformas.create.creating")
|
>
|
||||||
: t("recurring_proformas.create.submit")}
|
{mutation.isPending
|
||||||
</Button>
|
? t("recurring_proformas.create.creating")
|
||||||
</div>
|
: t("recurring_proformas.create.submit")}
|
||||||
}
|
</Button>
|
||||||
backLabel={t("recurring_proformas.create.cancel")}
|
</div>
|
||||||
onBack={() => navigate("/recurring-proformas")}
|
}
|
||||||
title={t("recurring_proformas.create.title")}
|
backLabel={t("recurring_proformas.create.cancel")}
|
||||||
/>
|
onBack={() => navigate("/recurring-proformas")}
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
title={t("recurring_proformas.create.title")}
|
||||||
<RdxEditorPageLayout width="simple">
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RdxPageContainer width="content">
|
||||||
|
<RdxEditorLayout>
|
||||||
<form
|
<form
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
id={RECURRING_PROFORMA_CREATE_FORM_ID}
|
id={RECURRING_PROFORMA_CREATE_FORM_ID}
|
||||||
@ -129,8 +137,8 @@ export const RecurringProformaCreatePage = () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</form>
|
</form>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</div>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,5 @@
|
|||||||
import { PageErrorState } from "@erp/core/components";
|
import { PageErrorState } from "@erp/core/components";
|
||||||
import {
|
import { RdxBadge, RdxPageContainer, RdxPageFrame, RdxPageStack } from "@repo/rdx-ui/components";
|
||||||
RdxBadge,
|
|
||||||
RdxEditorPageLayout,
|
|
||||||
RdxPageContainer,
|
|
||||||
RdxPageStack,
|
|
||||||
} from "@repo/rdx-ui/components";
|
|
||||||
import { Button, Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
import { Button, Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
||||||
import { ArrowLeftIcon } from "lucide-react";
|
import { ArrowLeftIcon } from "lucide-react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
@ -96,179 +91,181 @@ export const RecurringProformaDetailPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RdxPageStack className="fixed inset-0 min-h-0 overflow-hidden" spacing="none">
|
<RdxPageFrame
|
||||||
<ProformaPageHeader
|
header={
|
||||||
actions={
|
<ProformaPageHeader
|
||||||
<RecurringProformaDetailHeaderActions
|
actions={
|
||||||
actions={headerActions}
|
<RecurringProformaDetailHeaderActions
|
||||||
isPending={actionMutation.isPending}
|
actions={headerActions}
|
||||||
onExecuteAction={(action) =>
|
isPending={actionMutation.isPending}
|
||||||
actionMutation.mutate({ id: recurringProforma.id, action })
|
onExecuteAction={(action) =>
|
||||||
}
|
actionMutation.mutate({ id: recurringProforma.id, action })
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
backLabel={t("recurring_proformas.detail.back")}
|
}
|
||||||
onBack={() => navigate("/recurring-proformas")}
|
backLabel={t("recurring_proformas.detail.back")}
|
||||||
title={
|
onBack={() => navigate("/recurring-proformas")}
|
||||||
<div className="space-y-3">
|
title={
|
||||||
<span className="inline-flex max-w-full items-center gap-3">
|
<div className="space-y-3">
|
||||||
<span className="truncate">{mainView.name}</span>
|
<span className="inline-flex max-w-full items-center gap-3">
|
||||||
<RdxBadge
|
<span className="truncate">{mainView.name}</span>
|
||||||
className={`shrink-0 rounded-md px-4 py-3 ${getRecurringProformaStatusBadgeClassName(recurringProforma.status)}`}
|
<RdxBadge
|
||||||
variant="secondary"
|
className={`shrink-0 rounded-md px-4 py-3 ${getRecurringProformaStatusBadgeClassName(recurringProforma.status)}`}
|
||||||
>
|
variant="secondary"
|
||||||
{t(mainView.statusKey)}
|
>
|
||||||
</RdxBadge>
|
{t(mainView.statusKey)}
|
||||||
<RdxBadge className="shrink-0 px-4 py-3 rounded-md" variant="secondary">
|
</RdxBadge>
|
||||||
{t(
|
<RdxBadge className="shrink-0 px-4 py-3 rounded-md" variant="secondary">
|
||||||
recurringProforma.has_issue
|
{t(
|
||||||
? "recurring_proformas.list.issues.issue"
|
recurringProforma.has_issue
|
||||||
: "recurring_proformas.list.issues.none"
|
? "recurring_proformas.list.issues.issue"
|
||||||
)}
|
: "recurring_proformas.list.issues.none"
|
||||||
</RdxBadge>
|
)}
|
||||||
</span>
|
</RdxBadge>
|
||||||
<p className="text-sm text-muted-foreground">{metadata}</p>
|
</span>
|
||||||
</div>
|
<p className="text-sm text-muted-foreground">{metadata}</p>
|
||||||
}
|
</div>
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
<RdxPageContainer className="min-h-0 flex-1 overflow-y-auto" padding="none" width="wide">
|
}
|
||||||
<RdxEditorPageLayout width="simple">
|
>
|
||||||
|
<RdxPageContainer width="wide">
|
||||||
|
<RdxPageStack spacing="default">
|
||||||
<RecurringProformaOperationalSummary
|
<RecurringProformaOperationalSummary
|
||||||
locale={dateLocale}
|
locale={dateLocale}
|
||||||
recurringProforma={recurringProforma}
|
recurringProforma={recurringProforma}
|
||||||
/>
|
/>
|
||||||
</RdxEditorPageLayout>
|
|
||||||
|
|
||||||
<div className="grid gap-6 xl:grid-cols-2">
|
<div className="grid gap-6 xl:grid-cols-2">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("recurring_proformas.detail.general.title")}</CardTitle>
|
<CardTitle>{t("recurring_proformas.detail.general.title")}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{recurringProforma.description ? (
|
{recurringProforma.description ? (
|
||||||
<p className="text-sm leading-6 text-muted-foreground">
|
<p className="text-sm leading-6 text-muted-foreground">
|
||||||
{recurringProforma.description}
|
{recurringProforma.description}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("recurring_proformas.detail.schedule.title")}</CardTitle>
|
<CardTitle>{t("recurring_proformas.detail.schedule.title")}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{schedule ? (
|
{schedule ? (
|
||||||
|
<dl className="space-y-3">
|
||||||
|
<DetailField
|
||||||
|
label={t("recurring_proformas.detail.schedule.interval")}
|
||||||
|
value={mainView.scheduleLabel}
|
||||||
|
/>
|
||||||
|
<DetailField
|
||||||
|
label={t("recurring_proformas.detail.schedule.start_date")}
|
||||||
|
value={formatRecurringProformaDateOnly(schedule.start_date)}
|
||||||
|
/>
|
||||||
|
<DetailField
|
||||||
|
label={t("recurring_proformas.detail.schedule.end_date")}
|
||||||
|
value={
|
||||||
|
schedule.end_date
|
||||||
|
? formatRecurringProformaDateOnly(schedule.end_date)
|
||||||
|
: t("recurring_proformas.detail.schedule.no_end_date")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DetailField
|
||||||
|
label={t("recurring_proformas.detail.schedule.monthly_rule")}
|
||||||
|
value={
|
||||||
|
schedule.monthly_rule
|
||||||
|
? t(`recurring_proformas.list.schedule.${schedule.monthly_rule}`)
|
||||||
|
: t("recurring_proformas.detail.schedule.not_applicable")
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</dl>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("recurring_proformas.list.schedule.none")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t("recurring_proformas.detail.commercial.title")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
<dl className="space-y-3">
|
<dl className="space-y-3">
|
||||||
<DetailField
|
<DetailField
|
||||||
label={t("recurring_proformas.detail.schedule.interval")}
|
label={t("recurring_proformas.detail.commercial.payment_term")}
|
||||||
value={mainView.scheduleLabel}
|
value={formatRecurringProformaOverride(recurringProforma.payment_term_id, t)}
|
||||||
/>
|
/>
|
||||||
<DetailField
|
<DetailField
|
||||||
label={t("recurring_proformas.detail.schedule.start_date")}
|
label={t("recurring_proformas.detail.commercial.payment_method")}
|
||||||
value={formatRecurringProformaDateOnly(schedule.start_date)}
|
value={formatRecurringProformaOverride(recurringProforma.payment_method_id, t)}
|
||||||
/>
|
/>
|
||||||
<DetailField
|
<DetailField
|
||||||
label={t("recurring_proformas.detail.schedule.end_date")}
|
label={t("recurring_proformas.detail.commercial.language")}
|
||||||
value={
|
value={formatRecurringProformaOverride(recurringProforma.language, t)}
|
||||||
schedule.end_date
|
|
||||||
? formatRecurringProformaDateOnly(schedule.end_date)
|
|
||||||
: t("recurring_proformas.detail.schedule.no_end_date")
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<DetailField
|
<DetailField
|
||||||
label={t("recurring_proformas.detail.schedule.monthly_rule")}
|
label={t("recurring_proformas.detail.commercial.currency")}
|
||||||
value={
|
value={formatRecurringProformaOverride(recurringProforma.currency, t)}
|
||||||
schedule.monthly_rule
|
|
||||||
? t(`recurring_proformas.list.schedule.${schedule.monthly_rule}`)
|
|
||||||
: t("recurring_proformas.detail.schedule.not_applicable")
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<DetailField
|
||||||
|
label={t("recurring_proformas.detail.commercial.proforma_series")}
|
||||||
|
value={formatRecurringProformaOverride(recurringProforma.proforma_series, t)}
|
||||||
|
/>
|
||||||
|
<DetailField
|
||||||
|
label={t("recurring_proformas.detail.commercial.issued_invoice_series")}
|
||||||
|
value={formatRecurringProformaOverride(
|
||||||
|
recurringProforma.issued_invoice_series,
|
||||||
|
t
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{discount ? (
|
||||||
|
<DetailField
|
||||||
|
label={t("recurring_proformas.detail.commercial.global_discount")}
|
||||||
|
value={discount}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</dl>
|
</dl>
|
||||||
) : (
|
</CardContent>
|
||||||
<p className="text-sm text-muted-foreground">
|
</Card>
|
||||||
{t("recurring_proformas.list.schedule.none")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t("recurring_proformas.detail.commercial.title")}</CardTitle>
|
<CardTitle>{t("recurring_proformas.detail.items.title")}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<dl className="space-y-3">
|
{recurringProforma.items.length === 0 ? (
|
||||||
<DetailField
|
<p className="text-sm text-muted-foreground">
|
||||||
label={t("recurring_proformas.detail.commercial.payment_term")}
|
{t("recurring_proformas.detail.items.empty")}
|
||||||
value={formatRecurringProformaOverride(recurringProforma.payment_term_id, t)}
|
</p>
|
||||||
/>
|
) : (
|
||||||
<DetailField
|
<ol className="space-y-3">
|
||||||
label={t("recurring_proformas.detail.commercial.payment_method")}
|
{recurringProforma.items.map((item) => (
|
||||||
value={formatRecurringProformaOverride(recurringProforma.payment_method_id, t)}
|
<li className="flex gap-3 text-sm" key={item.id}>
|
||||||
/>
|
<span className="tabular-nums text-muted-foreground">{item.position}</span>
|
||||||
<DetailField
|
<span>
|
||||||
label={t("recurring_proformas.detail.commercial.language")}
|
{item.description ?? t("recurring_proformas.detail.items.no_description")}
|
||||||
value={formatRecurringProformaOverride(recurringProforma.language, t)}
|
</span>
|
||||||
/>
|
</li>
|
||||||
<DetailField
|
))}
|
||||||
label={t("recurring_proformas.detail.commercial.currency")}
|
</ol>
|
||||||
value={formatRecurringProformaOverride(recurringProforma.currency, t)}
|
)}
|
||||||
/>
|
</CardContent>
|
||||||
<DetailField
|
</Card>
|
||||||
label={t("recurring_proformas.detail.commercial.proforma_series")}
|
</div>
|
||||||
value={formatRecurringProformaOverride(recurringProforma.proforma_series, t)}
|
|
||||||
/>
|
|
||||||
<DetailField
|
|
||||||
label={t("recurring_proformas.detail.commercial.issued_invoice_series")}
|
|
||||||
value={formatRecurringProformaOverride(
|
|
||||||
recurringProforma.issued_invoice_series,
|
|
||||||
t
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{discount ? (
|
|
||||||
<DetailField
|
|
||||||
label={t("recurring_proformas.detail.commercial.global_discount")}
|
|
||||||
value={discount}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</dl>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<RecurringProformaOccurrencesSection recurringProformaId={recurringProforma.id} />
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{t("recurring_proformas.detail.items.title")}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{recurringProforma.items.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t("recurring_proformas.detail.items.empty")}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<ol className="space-y-3">
|
|
||||||
{recurringProforma.items.map((item) => (
|
|
||||||
<li className="flex gap-3 text-sm" key={item.id}>
|
|
||||||
<span className="tabular-nums text-muted-foreground">{item.position}</span>
|
|
||||||
<span>
|
|
||||||
{item.description ?? t("recurring_proformas.detail.items.no_description")}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<RecurringProformaOccurrencesSection recurringProformaId={recurringProforma.id} />
|
<Button onClick={() => navigate("/recurring-proformas")} type="button" variant="outline">
|
||||||
|
<ArrowLeftIcon aria-hidden className="size-4" />
|
||||||
<Button onClick={() => navigate("/recurring-proformas")} type="button" variant="outline">
|
{t("recurring_proformas.detail.back")}
|
||||||
<ArrowLeftIcon aria-hidden className="size-4" />
|
</Button>
|
||||||
{t("recurring_proformas.detail.back")}
|
</RdxPageStack>
|
||||||
</Button>
|
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
</RdxPageStack>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -124,6 +124,7 @@ mock.module("./controllers/use-list-recurring-proformas-page-controller", {
|
|||||||
mock.module("@repo/rdx-ui/components", {
|
mock.module("@repo/rdx-ui/components", {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
RdxPageContainer: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
RdxPageContainer: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
|
RdxPageFrame: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
RdxPageStack: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
RdxPageStack: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
||||||
RdxPageHeader: ({
|
RdxPageHeader: ({
|
||||||
title,
|
title,
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
CircleIcon,
|
CircleIcon,
|
||||||
CirclePauseIcon,
|
CirclePauseIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
|
PencilIcon,
|
||||||
PlayCircleIcon,
|
PlayCircleIcon,
|
||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@ -63,6 +64,7 @@ const actionIcons = {
|
|||||||
complete: CheckCircle2Icon,
|
complete: CheckCircle2Icon,
|
||||||
archive: ArchiveIcon,
|
archive: ArchiveIcon,
|
||||||
duplicate: CopyIcon,
|
duplicate: CopyIcon,
|
||||||
|
edit: PencilIcon,
|
||||||
delete: Trash2Icon,
|
delete: Trash2Icon,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { ErrorAlert } from "@erp/core/components";
|
|||||||
import {
|
import {
|
||||||
RdxPageActions,
|
RdxPageActions,
|
||||||
RdxPageContainer,
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
RdxPageHeader,
|
RdxPageHeader,
|
||||||
RdxPageStack,
|
RdxPageStack,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
@ -28,20 +29,22 @@ export const ScopedListRecurringProformasPageView = ({
|
|||||||
content,
|
content,
|
||||||
}: ScopedListRecurringProformasPageViewProps) => {
|
}: ScopedListRecurringProformasPageViewProps) => {
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<RdxPageStack spacing="default">
|
<RdxPageContainer width="full">
|
||||||
<RdxPageHeader
|
<RdxPageStack spacing="default">
|
||||||
actions={primaryAction ? <RdxPageActions primary={primaryAction} /> : undefined}
|
<RdxPageHeader
|
||||||
description={description}
|
actions={primaryAction ? <RdxPageActions primary={primaryAction} /> : undefined}
|
||||||
title={title}
|
description={description}
|
||||||
/>
|
title={title}
|
||||||
|
/>
|
||||||
|
|
||||||
{refreshError ? (
|
{refreshError ? (
|
||||||
<ErrorAlert message={refreshError.message} title={refreshError.title} />
|
<ErrorAlert message={refreshError.message} title={refreshError.title} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{content}
|
{content}
|
||||||
</RdxPageStack>
|
</RdxPageStack>
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./recurring-proforma-layout";
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
|
||||||
|
|
||||||
export function RecurringProformaLayout({ children }: PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-full w-full" id="recurring_proformas ">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import { PageFormHeader, PageKeyboardShortcutsButton } from "@erp/core/components";
|
import { PageKeyboardShortcutsButton } from "@erp/core/components";
|
||||||
import {
|
import {
|
||||||
CancelActionButton,
|
CancelActionButton,
|
||||||
FormActionsBar,
|
FormActionsBar,
|
||||||
type FormSecondaryAction,
|
type FormSecondaryAction,
|
||||||
FormSecondaryActionsMenu,
|
FormSecondaryActionsMenu,
|
||||||
|
RdxEditorHeader,
|
||||||
RhfSubmitActionButton,
|
RhfSubmitActionButton,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
@ -74,7 +75,7 @@ export const CustomerCreateHeader = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageFormHeader
|
<RdxEditorHeader
|
||||||
actions={
|
actions={
|
||||||
<FormActionsBar align="end" reverseOnMobile={false}>
|
<FormActionsBar align="end" reverseOnMobile={false}>
|
||||||
<PageKeyboardShortcutsButton
|
<PageKeyboardShortcutsButton
|
||||||
@ -113,10 +114,10 @@ export const CustomerCreateHeader = ({
|
|||||||
className={className}
|
className={className}
|
||||||
onBack={onCancel}
|
onBack={onCancel}
|
||||||
showStatus={hasChanges}
|
showStatus={hasChanges}
|
||||||
statusLabel={labels.modified}
|
status={labels.modified}
|
||||||
title={labels.title}
|
title={labels.title}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</PageFormHeader>
|
</RdxEditorHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { BackHistoryButton, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
@ -6,8 +6,8 @@ import { useTranslation } from "../../../i18n";
|
|||||||
export const CustomerEditorSkeleton = () => {
|
export const CustomerEditorSkeleton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<>
|
<RdxPageFrame>
|
||||||
<AppContent>
|
<RdxPageContainer width="content">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div aria-hidden="true" className="space-y-2">
|
<div aria-hidden="true" className="space-y-2">
|
||||||
<div className="h-7 w-64 rounded-md bg-muted animate-pulse" />
|
<div className="h-7 w-64 rounded-md bg-muted animate-pulse" />
|
||||||
@ -26,7 +26,7 @@ export const CustomerEditorSkeleton = () => {
|
|||||||
<div className="h-28 w-full rounded-md bg-muted animate-pulse" />
|
<div className="h-28 w-full rounded-md bg-muted animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
<span className="sr-only">{t("update.loading")}</span>
|
<span className="sr-only">{t("update.loading")}</span>
|
||||||
</AppContent>
|
</RdxPageContainer>
|
||||||
</>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ErrorAlert } from "@erp/core/components";
|
import { ErrorAlert } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { RdxEditorPageLayout } from "@repo/rdx-ui/components";
|
import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
@ -19,32 +19,35 @@ export const CustomerCreatePage = () => {
|
|||||||
const handleCancel = () => navigateBack();
|
const handleCancel = () => navigateBack();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
<FormProvider {...createCtrl.form}>
|
||||||
<FormProvider {...createCtrl.form}>
|
<UnsavedChangesProvider isDirty={createCtrl.form.formState.isDirty}>
|
||||||
<UnsavedChangesProvider isDirty={createCtrl.form.formState.isDirty}>
|
<RdxPageFrame
|
||||||
<CustomerCreateHeader
|
contentClassName="bg-muted/20"
|
||||||
formId={createCtrl.formId}
|
header={
|
||||||
hasChanges={createCtrl.form.formState.isDirty}
|
<CustomerCreateHeader
|
||||||
isSaving={createCtrl.form.formState.isSubmitting}
|
formId={createCtrl.formId}
|
||||||
labels={{
|
hasChanges={createCtrl.form.formState.isDirty}
|
||||||
title: t("create.title"),
|
isSaving={createCtrl.form.formState.isSubmitting}
|
||||||
back: t("create.header.back"),
|
labels={{
|
||||||
modified: t("create.header.modified"),
|
title: t("create.title"),
|
||||||
cancel: t("common.actions.cancel"),
|
back: t("create.header.back"),
|
||||||
save: t("common.actions.save"),
|
modified: t("create.header.modified"),
|
||||||
saving: t("common.actions.saving"),
|
cancel: t("common.actions.cancel"),
|
||||||
moreActions: t("common.actions.more"),
|
save: t("common.actions.save"),
|
||||||
keyboardShortcuts: t("create.header.keyboard_shortcuts"),
|
saving: t("common.actions.saving"),
|
||||||
delete: t("create.header.delete"),
|
moreActions: t("common.actions.more"),
|
||||||
}}
|
keyboardShortcuts: t("create.header.keyboard_shortcuts"),
|
||||||
onCancel={handleCancel}
|
delete: t("create.header.delete"),
|
||||||
//onDelete={openDeleteDialog}
|
}}
|
||||||
//onDuplicate={handleDuplicate}
|
onCancel={handleCancel}
|
||||||
//onExportPdf={handleExportPdf}
|
//onDelete={openDeleteDialog}
|
||||||
/>
|
//onDuplicate={handleDuplicate}
|
||||||
|
//onExportPdf={handleExportPdf}
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
/>
|
||||||
<RdxEditorPageLayout width="simple">
|
}
|
||||||
|
>
|
||||||
|
<RdxPageContainer width="content">
|
||||||
|
<RdxEditorLayout>
|
||||||
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
<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">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
{createCtrl.isCreateError ? (
|
{createCtrl.isCreateError ? (
|
||||||
@ -65,10 +68,10 @@ export const CustomerCreatePage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</UnsavedChangesProvider>
|
</RdxPageFrame>
|
||||||
</FormProvider>
|
</UnsavedChangesProvider>
|
||||||
</div>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,10 +2,6 @@ import type { ModuleClientParams } from "@erp/core/client";
|
|||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
import { Outlet, type RouteObject } from "react-router-dom";
|
import { Outlet, type RouteObject } from "react-router-dom";
|
||||||
|
|
||||||
// Lazy load components
|
|
||||||
const CustomerLayout = lazy(() =>
|
|
||||||
import("./shared/ui").then((m) => ({ default: m.CustomerLayout }))
|
|
||||||
);
|
|
||||||
const CustomersListPage = lazy(() =>
|
const CustomersListPage = lazy(() =>
|
||||||
import("./list").then((m) => ({ default: m.ListCustomersPage }))
|
import("./list").then((m) => ({ default: m.ListCustomersPage }))
|
||||||
);
|
);
|
||||||
@ -28,11 +24,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
|||||||
layout: "app-sidebar",
|
layout: "app-sidebar",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <Outlet context={params} />,
|
||||||
<CustomerLayout>
|
|
||||||
<Outlet context={params} />
|
|
||||||
</CustomerLayout>
|
|
||||||
),
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
@ -51,11 +43,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <CustomerCreatePage />,
|
||||||
<CustomerLayout>
|
|
||||||
<CustomerCreatePage />
|
|
||||||
</CustomerLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -64,11 +52,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <CustomerUpdatePage />,
|
||||||
<CustomerLayout>
|
|
||||||
<CustomerUpdatePage />
|
|
||||||
</CustomerLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -77,11 +61,7 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
|||||||
layout: "app-fullscreen",
|
layout: "app-fullscreen",
|
||||||
protected: true,
|
protected: true,
|
||||||
},
|
},
|
||||||
element: (
|
element: <CustomerViewPage />,
|
||||||
<CustomerLayout>
|
|
||||||
<CustomerViewPage />
|
|
||||||
</CustomerLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { ErrorAlert } from "@erp/core/components";
|
|||||||
import {
|
import {
|
||||||
RdxPageActions,
|
RdxPageActions,
|
||||||
RdxPageContainer,
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
RdxPageHeader,
|
RdxPageHeader,
|
||||||
RdxPageStack,
|
RdxPageStack,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
@ -30,22 +31,24 @@ export const ListCustomersPageView = ({
|
|||||||
dialogs,
|
dialogs,
|
||||||
}: ListCustomersPageViewProps) => {
|
}: ListCustomersPageViewProps) => {
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<RdxPageStack spacing="default">
|
<RdxPageContainer width="full">
|
||||||
<RdxPageHeader
|
<RdxPageStack spacing="default">
|
||||||
actions={primaryAction ? <RdxPageActions primary={primaryAction} /> : undefined}
|
<RdxPageHeader
|
||||||
description={description}
|
actions={primaryAction ? <RdxPageActions primary={primaryAction} /> : undefined}
|
||||||
title={title}
|
description={description}
|
||||||
/>
|
title={title}
|
||||||
|
/>
|
||||||
|
|
||||||
{refreshError ? (
|
{refreshError ? (
|
||||||
<ErrorAlert message={refreshError.message} title={refreshError.title} />
|
<ErrorAlert message={refreshError.message} title={refreshError.title} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{content}
|
{content}
|
||||||
</RdxPageStack>
|
</RdxPageStack>
|
||||||
|
|
||||||
{dialogs}
|
{dialogs}
|
||||||
</RdxPageContainer>
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { PageErrorState, SimpleSearchInput } from "@erp/core/components";
|
import { PageErrorState, SimpleSearchInput } from "@erp/core/components";
|
||||||
import { useReturnToNavigation } from "@erp/core/hooks";
|
import { useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { RdxPageContainer } from "@repo/rdx-ui/components";
|
import { RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
@ -14,6 +14,7 @@ import { createSearchParams, useNavigate } from "react-router-dom";
|
|||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { useListCustomersPageController } from "../../controllers";
|
import { useListCustomersPageController } from "../../controllers";
|
||||||
import { CustomerSummaryPanel, CustomersGrid, useCustomersGridColumns } from "../blocks";
|
import { CustomerSummaryPanel, CustomersGrid, useCustomersGridColumns } from "../blocks";
|
||||||
|
|
||||||
import { ListCustomersPageView } from "./list-customers-page-view";
|
import { ListCustomersPageView } from "./list-customers-page-view";
|
||||||
|
|
||||||
export const ListCustomersPage = () => {
|
export const ListCustomersPage = () => {
|
||||||
@ -90,8 +91,8 @@ export const ListCustomersPage = () => {
|
|||||||
|
|
||||||
if (pageError) {
|
if (pageError) {
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<div className="mx-auto w-full max-w-3xl">
|
<RdxPageContainer width="content">
|
||||||
<PageErrorState
|
<PageErrorState
|
||||||
description={pageError.description}
|
description={pageError.description}
|
||||||
primaryAction={
|
primaryAction={
|
||||||
@ -110,8 +111,8 @@ export const ListCustomersPage = () => {
|
|||||||
title={pageError.title}
|
title={pageError.title}
|
||||||
variant={pageError.variant}
|
variant={pageError.variant}
|
||||||
/>
|
/>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</RdxPageContainer>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,14 +158,14 @@ export const ListCustomersPage = () => {
|
|||||||
}
|
}
|
||||||
description={t("list.description")}
|
description={t("list.description")}
|
||||||
primaryAction={
|
primaryAction={
|
||||||
<Button
|
<Button
|
||||||
aria-label={t("common.actions.create")}
|
aria-label={t("common.actions.create")}
|
||||||
onClick={() => navigate("/customers/create")}
|
onClick={() => navigate("/customers/create")}
|
||||||
size="default"
|
size="default"
|
||||||
>
|
>
|
||||||
<PlusIcon aria-hidden className="mr-2 size-4" />
|
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||||
{t("common.actions.create")}
|
{t("common.actions.create")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
refreshError={refreshError}
|
refreshError={refreshError}
|
||||||
title={t("list.title")}
|
title={t("list.title")}
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
|
||||||
|
|
||||||
export const CustomerLayout = ({ children }: PropsWithChildren) => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col h-full w-full" id="customers">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./customer-layout";
|
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import { PageFormHeader, PageKeyboardShortcutsButton } from "@erp/core/components";
|
import { PageKeyboardShortcutsButton } from "@erp/core/components";
|
||||||
import {
|
import {
|
||||||
CancelActionButton,
|
CancelActionButton,
|
||||||
FormActionsBar,
|
FormActionsBar,
|
||||||
type FormSecondaryAction,
|
type FormSecondaryAction,
|
||||||
FormSecondaryActionsMenu,
|
FormSecondaryActionsMenu,
|
||||||
|
RdxEditorHeader,
|
||||||
RhfSubmitActionButton,
|
RhfSubmitActionButton,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
@ -75,7 +76,7 @@ export const CustomerUpdateHeader = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageFormHeader
|
<RdxEditorHeader
|
||||||
actions={
|
actions={
|
||||||
<FormActionsBar align="end" reverseOnMobile={false}>
|
<FormActionsBar align="end" reverseOnMobile={false}>
|
||||||
<PageKeyboardShortcutsButton
|
<PageKeyboardShortcutsButton
|
||||||
@ -114,10 +115,10 @@ export const CustomerUpdateHeader = ({
|
|||||||
className={className}
|
className={className}
|
||||||
onBack={onCancel}
|
onBack={onCancel}
|
||||||
showStatus={hasChanges}
|
showStatus={hasChanges}
|
||||||
statusLabel={labels.modified}
|
status={labels.modified}
|
||||||
title={labels.title}
|
title={labels.title}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</PageFormHeader>
|
</RdxEditorHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { BackHistoryButton, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
@ -6,8 +6,8 @@ import { useTranslation } from "../../../i18n";
|
|||||||
export const CustomerEditorSkeleton = () => {
|
export const CustomerEditorSkeleton = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<>
|
<RdxPageFrame>
|
||||||
<AppContent>
|
<RdxPageContainer width="content">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div aria-hidden="true" className="space-y-2">
|
<div aria-hidden="true" className="space-y-2">
|
||||||
<div className="h-7 w-64 rounded-md bg-muted animate-pulse" />
|
<div className="h-7 w-64 rounded-md bg-muted animate-pulse" />
|
||||||
@ -26,7 +26,7 @@ export const CustomerEditorSkeleton = () => {
|
|||||||
<div className="h-28 w-full rounded-md bg-muted animate-pulse" />
|
<div className="h-28 w-full rounded-md bg-muted animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
<span className="sr-only">{t("update.loading")}</span>
|
<span className="sr-only">{t("update.loading")}</span>
|
||||||
</AppContent>
|
</RdxPageContainer>
|
||||||
</>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ErrorAlert, PageErrorState } from "@erp/core/components";
|
import { ErrorAlert, PageErrorState } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { RdxEditorPageLayout } from "@repo/rdx-ui/components";
|
import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
@ -26,8 +26,8 @@ export const CustomerUpdatePage = () => {
|
|||||||
|
|
||||||
if (ctrl.pageError) {
|
if (ctrl.pageError) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 sm:p-6">
|
<RdxPageFrame>
|
||||||
<div className="mx-auto w-full max-w-3xl">
|
<RdxPageContainer width="content">
|
||||||
<PageErrorState
|
<PageErrorState
|
||||||
description={ctrl.pageError.description}
|
description={ctrl.pageError.description}
|
||||||
primaryAction={
|
primaryAction={
|
||||||
@ -61,39 +61,42 @@ export const CustomerUpdatePage = () => {
|
|||||||
title={ctrl.pageError.title}
|
title={ctrl.pageError.title}
|
||||||
variant={ctrl.pageError.variant}
|
variant={ctrl.pageError.variant}
|
||||||
/>
|
/>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</div>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
<FormProvider {...updateCtrl.form}>
|
||||||
<FormProvider {...updateCtrl.form}>
|
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
||||||
<UnsavedChangesProvider isDirty={updateCtrl.form.formState.isDirty}>
|
<RdxPageFrame
|
||||||
<CustomerUpdateHeader
|
contentClassName="bg-muted/20"
|
||||||
formId={updateCtrl.formId}
|
header={
|
||||||
hasChanges={updateCtrl.form.formState.isDirty}
|
<CustomerUpdateHeader
|
||||||
isSaving={updateCtrl.form.formState.isSubmitting}
|
formId={updateCtrl.formId}
|
||||||
labels={{
|
hasChanges={updateCtrl.form.formState.isDirty}
|
||||||
title: "Editar cliente",
|
isSaving={updateCtrl.form.formState.isSubmitting}
|
||||||
back: "Volver",
|
labels={{
|
||||||
modified: "Modificada",
|
title: "Editar cliente",
|
||||||
cancel: "Cancelar",
|
back: "Volver",
|
||||||
save: "Guardar",
|
modified: "Modificada",
|
||||||
saving: "Guardando...",
|
cancel: "Cancelar",
|
||||||
moreActions: "Más acciones",
|
save: "Guardar",
|
||||||
keyboardShortcuts: "Ver atajos de teclado",
|
saving: "Guardando...",
|
||||||
duplicate: "Duplicar cliente",
|
moreActions: "Más acciones",
|
||||||
delete: "Eliminar",
|
keyboardShortcuts: "Ver atajos de teclado",
|
||||||
}}
|
duplicate: "Duplicar cliente",
|
||||||
onCancel={handleCancel}
|
delete: "Eliminar",
|
||||||
//onDelete={openDeleteDialog}
|
}}
|
||||||
//onDuplicate={handleDuplicate}
|
onCancel={handleCancel}
|
||||||
//onExportPdf={handleExportPdf}
|
//onDelete={openDeleteDialog}
|
||||||
/>
|
//onDuplicate={handleDuplicate}
|
||||||
|
//onExportPdf={handleExportPdf}
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
/>
|
||||||
<RdxEditorPageLayout width="simple">
|
}
|
||||||
|
>
|
||||||
|
<RdxPageContainer width="content">
|
||||||
|
<RdxEditorLayout>
|
||||||
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
<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">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
{updateCtrl.isUpdateError ? (
|
{updateCtrl.isUpdateError ? (
|
||||||
@ -121,10 +124,10 @@ export const CustomerUpdatePage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</UnsavedChangesProvider>
|
</RdxPageFrame>
|
||||||
</FormProvider>
|
</UnsavedChangesProvider>
|
||||||
</div>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { PageFormHeader, PageKeyboardShortcutsButton } from "@erp/core/components";
|
import { PageKeyboardShortcutsButton } from "@erp/core/components";
|
||||||
import {
|
import {
|
||||||
CancelActionButton,
|
CancelActionButton,
|
||||||
FormActionsBar,
|
FormActionsBar,
|
||||||
type FormSecondaryAction,
|
type FormSecondaryAction,
|
||||||
FormSecondaryActionsMenu,
|
FormSecondaryActionsMenu,
|
||||||
|
RdxEditorHeader,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ export const CustomerViewHeader = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageFormHeader
|
<RdxEditorHeader
|
||||||
actions={
|
actions={
|
||||||
<FormActionsBar align="end" reverseOnMobile={false}>
|
<FormActionsBar align="end" reverseOnMobile={false}>
|
||||||
<PageKeyboardShortcutsButton
|
<PageKeyboardShortcutsButton
|
||||||
@ -101,10 +102,10 @@ export const CustomerViewHeader = ({
|
|||||||
className={className}
|
className={className}
|
||||||
onBack={onCancel}
|
onBack={onCancel}
|
||||||
showStatus={hasChanges}
|
showStatus={hasChanges}
|
||||||
statusLabel={labels.modified}
|
status={labels.modified}
|
||||||
title={labels.title}
|
title={labels.title}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</PageFormHeader>
|
</RdxEditorHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +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 { Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
import { Card, CardContent, CardHeader, CardTitle } from "@repo/shadcn-ui/components";
|
||||||
import {
|
import {
|
||||||
Banknote,
|
Banknote,
|
||||||
@ -35,8 +36,8 @@ export const CustomerViewPage = () => {
|
|||||||
|
|
||||||
if (ctrl.pageError) {
|
if (ctrl.pageError) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 sm:p-6">
|
<RdxPageFrame>
|
||||||
<div className="mx-auto w-full max-w-3xl">
|
<RdxPageContainer width="content">
|
||||||
<PageErrorState
|
<PageErrorState
|
||||||
description={ctrl.pageError.description}
|
description={ctrl.pageError.description}
|
||||||
primaryAction={
|
primaryAction={
|
||||||
@ -70,32 +71,34 @@ export const CustomerViewPage = () => {
|
|||||||
title={ctrl.pageError.title}
|
title={ctrl.pageError.title}
|
||||||
variant={ctrl.pageError.variant}
|
variant={ctrl.pageError.variant}
|
||||||
/>
|
/>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</div>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
<RdxPageFrame
|
||||||
<CustomerViewHeader
|
header={
|
||||||
labels={{
|
<CustomerViewHeader
|
||||||
title: "Ver cliente",
|
labels={{
|
||||||
back: "Volver",
|
title: "Ver cliente",
|
||||||
modified: "Modificada",
|
back: "Volver",
|
||||||
cancel: "Cancelar",
|
modified: "Modificada",
|
||||||
save: "Guardar",
|
cancel: "Cancelar",
|
||||||
saving: "Guardando...",
|
save: "Guardar",
|
||||||
moreActions: "Más acciones",
|
saving: "Guardando...",
|
||||||
keyboardShortcuts: "Ver atajos de teclado",
|
moreActions: "Más acciones",
|
||||||
delete: "Eliminar",
|
keyboardShortcuts: "Ver atajos de teclado",
|
||||||
}}
|
delete: "Eliminar",
|
||||||
onCancel={handleCancel}
|
}}
|
||||||
//onDelete={openDeleteDialog}
|
onCancel={handleCancel}
|
||||||
//onDuplicate={handleDuplicate}
|
//onDelete={openDeleteDialog}
|
||||||
//onExportPdf={handleExportPdf}
|
//onDuplicate={handleDuplicate}
|
||||||
/>
|
//onExportPdf={handleExportPdf}
|
||||||
|
/>
|
||||||
<div className="flex-1 overflow-y-auto">
|
}
|
||||||
|
>
|
||||||
|
<RdxPageContainer className="space-y-6" width="content">
|
||||||
{/* Información Básica */}
|
{/* Información Básica */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -369,7 +372,7 @@ export const CustomerViewPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</div>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -88,7 +88,7 @@ Contenido visible actual:
|
|||||||
|
|
||||||
- información personal: `name`, `email`, `language_code`
|
- información personal: `name`, `email`, `language_code`
|
||||||
- modo `app-fullscreen`, sin sidebar principal
|
- modo `app-fullscreen`, sin sidebar principal
|
||||||
- contenido centrado con ancho controlado mediante `RdxEditorPageLayout`
|
- contenido centrado mediante `RdxPageContainer` y compuesto con `RdxEditorLayout`
|
||||||
- seguridad mediante dialog para cambio de contraseña
|
- seguridad mediante dialog para cambio de contraseña
|
||||||
- cierre de sesión reutilizando el flujo de logout existente
|
- cierre de sesión reutilizando el flujo de logout existente
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,65 @@
|
|||||||
|
import { PageErrorState } from "@erp/core/components";
|
||||||
|
import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
|
export const AccountLoadingState = ({ label }: { label: string }) => (
|
||||||
|
<RdxPageFrame contentClassName="bg-muted/20">
|
||||||
|
<RdxPageContainer className="min-h-full" width="content">
|
||||||
|
<RdxEditorLayout>
|
||||||
|
<div className="flex min-h-[16rem] items-center justify-center text-sm text-muted-foreground">
|
||||||
|
<div aria-busy="true" role="status">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RdxEditorLayout>
|
||||||
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface AccountErrorStateProps {
|
||||||
|
canRetry: boolean;
|
||||||
|
description: string;
|
||||||
|
isRetrying: boolean;
|
||||||
|
onRetry: () => void;
|
||||||
|
reference?: string;
|
||||||
|
retryLabel: string;
|
||||||
|
retryingLabel: string;
|
||||||
|
title: string;
|
||||||
|
variant: ComponentProps<typeof PageErrorState>["variant"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccountErrorState = ({
|
||||||
|
canRetry,
|
||||||
|
description,
|
||||||
|
isRetrying,
|
||||||
|
onRetry,
|
||||||
|
reference,
|
||||||
|
retryLabel,
|
||||||
|
retryingLabel,
|
||||||
|
title,
|
||||||
|
variant,
|
||||||
|
}: AccountErrorStateProps) => (
|
||||||
|
<RdxPageFrame contentClassName="bg-muted/20">
|
||||||
|
<RdxPageContainer width="content">
|
||||||
|
<RdxEditorLayout>
|
||||||
|
<div className="mx-auto w-full max-w-3xl">
|
||||||
|
<PageErrorState
|
||||||
|
description={description}
|
||||||
|
primaryAction={
|
||||||
|
canRetry
|
||||||
|
? {
|
||||||
|
label: isRetrying ? retryingLabel : retryLabel,
|
||||||
|
onClick: onRetry,
|
||||||
|
isLoading: isRetrying,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
reference={reference}
|
||||||
|
title={title}
|
||||||
|
variant={variant}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RdxEditorLayout>
|
||||||
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
|
);
|
||||||
@ -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(<AccountLoadingState label="Cargando cuenta" />);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<AccountErrorState
|
||||||
|
canRetry
|
||||||
|
description="No se pudo cargar la cuenta"
|
||||||
|
isRetrying={false}
|
||||||
|
onRetry={() => 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/);
|
||||||
|
});
|
||||||
@ -1,9 +1,12 @@
|
|||||||
import { ErrorAlert, PageErrorState, PageFormHeader } from "@erp/core/components";
|
import { ErrorAlert } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import {
|
import {
|
||||||
CancelActionButton,
|
CancelActionButton,
|
||||||
RdxEditorActionBar,
|
RdxEditorActionBar,
|
||||||
RdxEditorPageLayout,
|
RdxEditorHeader,
|
||||||
|
RdxEditorLayout,
|
||||||
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
RdxPanel,
|
RdxPanel,
|
||||||
RhfSubmitActionButton,
|
RhfSubmitActionButton,
|
||||||
SelectField,
|
SelectField,
|
||||||
@ -26,65 +29,7 @@ import { useTranslation } from "../../../i18n";
|
|||||||
import { useAccountPageController } from "../../controllers";
|
import { useAccountPageController } from "../../controllers";
|
||||||
import type { AccountPasswordForm, AccountProfileForm } from "../../entities";
|
import type { AccountPasswordForm, AccountProfileForm } from "../../entities";
|
||||||
|
|
||||||
const AccountLoadingState = ({ label }: { label: string }) => {
|
import { AccountErrorState, AccountLoadingState } from "./account-page-states";
|
||||||
return (
|
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
|
||||||
<RdxEditorPageLayout className="min-h-full" width="simple">
|
|
||||||
<div className="flex min-h-[16rem] items-center justify-center text-sm text-muted-foreground">
|
|
||||||
<div aria-busy="true" role="status">
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</RdxEditorPageLayout>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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<typeof PageErrorState>["variant"];
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
|
||||||
<RdxEditorPageLayout width="simple">
|
|
||||||
<div className="mx-auto w-full max-w-3xl">
|
|
||||||
<PageErrorState
|
|
||||||
description={description}
|
|
||||||
primaryAction={
|
|
||||||
canRetry
|
|
||||||
? {
|
|
||||||
label: isRetrying ? retryingLabel : retryLabel,
|
|
||||||
onClick: onRetry,
|
|
||||||
isLoading: isRetrying,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
reference={reference}
|
|
||||||
title={title}
|
|
||||||
variant={variant}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</RdxEditorPageLayout>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AccountPasswordDialog = ({
|
const AccountPasswordDialog = ({
|
||||||
ctrl,
|
ctrl,
|
||||||
@ -251,15 +196,18 @@ export const AccountPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
<RdxPageFrame
|
||||||
<PageFormHeader
|
contentClassName="bg-muted/20"
|
||||||
backLabel={t("account.header.back")}
|
header={
|
||||||
onBack={navigateBack}
|
<RdxEditorHeader
|
||||||
title={t("account.title")}
|
backLabel={t("account.header.back")}
|
||||||
/>
|
onBack={navigateBack}
|
||||||
|
title={t("account.title")}
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
/>
|
||||||
<RdxEditorPageLayout width="simple">
|
}
|
||||||
|
>
|
||||||
|
<RdxPageContainer width="content">
|
||||||
|
<RdxEditorLayout>
|
||||||
<div className="rounded-2xl border border-border/60 bg-background px-4 py-4 shadow-xs md:px-6 md:py-6">
|
<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">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
<FormProvider {...ctrl.profileForm}>
|
<FormProvider {...ctrl.profileForm}>
|
||||||
@ -404,8 +352,8 @@ export const AccountPage = () => {
|
|||||||
</RdxPanel>
|
</RdxPanel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
|
|
||||||
<AccountPasswordDialog
|
<AccountPasswordDialog
|
||||||
ctrl={ctrl}
|
ctrl={ctrl}
|
||||||
@ -414,6 +362,6 @@ export const AccountPage = () => {
|
|||||||
onOpenChange={handlePasswordDialogOpenChange}
|
onOpenChange={handlePasswordDialogOpenChange}
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
</div>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ErrorAlert } from "@erp/core/components";
|
import { ErrorAlert } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { RdxEditorPageLayout } from "@repo/rdx-ui/components";
|
import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
import { createSearchParams, useNavigate, useSearchParams } from "react-router-dom";
|
import { createSearchParams, useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
@ -24,18 +24,22 @@ export const SupplierCreatePage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
<FormProvider {...controller.form}>
|
||||||
<FormProvider {...controller.form}>
|
<UnsavedChangesProvider isDirty={controller.form.formState.isDirty}>
|
||||||
<UnsavedChangesProvider isDirty={controller.form.formState.isDirty}>
|
<RdxPageFrame
|
||||||
<SupplierEditorHeader
|
contentClassName="bg-muted/20"
|
||||||
formId={controller.formId}
|
header={
|
||||||
hasChanges={controller.form.formState.isDirty}
|
<SupplierEditorHeader
|
||||||
isSaving={controller.mutation.isPending}
|
formId={controller.formId}
|
||||||
onCancel={navigateBack}
|
hasChanges={controller.form.formState.isDirty}
|
||||||
title={t("create.title")}
|
isSaving={controller.mutation.isPending}
|
||||||
/>
|
onCancel={navigateBack}
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
title={t("create.title")}
|
||||||
<RdxEditorPageLayout width="simple">
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RdxPageContainer width="content">
|
||||||
|
<RdxEditorLayout>
|
||||||
<div className="rounded-2xl border border-border/60 bg-background p-4 shadow-xs md:p-6">
|
<div className="rounded-2xl border border-border/60 bg-background p-4 shadow-xs md:p-6">
|
||||||
<div className="flex min-w-0 flex-col gap-6">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
{controller.mutation.isError ? (
|
{controller.mutation.isError ? (
|
||||||
@ -51,10 +55,10 @@ export const SupplierCreatePage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</UnsavedChangesProvider>
|
</RdxPageFrame>
|
||||||
</FormProvider>
|
</UnsavedChangesProvider>
|
||||||
</div>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { PageErrorState, SimpleSearchInput } from "@erp/core/components";
|
|||||||
import {
|
import {
|
||||||
RdxPageActions,
|
RdxPageActions,
|
||||||
RdxPageContainer,
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
RdxPageHeader,
|
RdxPageHeader,
|
||||||
RdxPageStack,
|
RdxPageStack,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
@ -36,63 +37,67 @@ export const SuppliersListPage = () => {
|
|||||||
|
|
||||||
if (controller.isError && controller.data.items.length === 0) {
|
if (controller.isError && controller.data.items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<PageErrorState
|
<RdxPageContainer width="content">
|
||||||
description={t("list.error.description")}
|
<PageErrorState
|
||||||
primaryAction={{
|
description={t("list.error.description")}
|
||||||
label: t("common.actions.retry"),
|
primaryAction={{
|
||||||
onClick: async () => {
|
label: t("common.actions.retry"),
|
||||||
await controller.refetch();
|
onClick: async () => {
|
||||||
},
|
await controller.refetch();
|
||||||
}}
|
},
|
||||||
title={t("list.error.title")}
|
}}
|
||||||
variant="unknown"
|
title={t("list.error.title")}
|
||||||
/>
|
variant="unknown"
|
||||||
</RdxPageContainer>
|
/>
|
||||||
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<RdxPageStack spacing="default">
|
<RdxPageContainer width="full">
|
||||||
<RdxPageHeader
|
<RdxPageStack spacing="default">
|
||||||
actions={
|
<RdxPageHeader
|
||||||
<RdxPageActions
|
actions={
|
||||||
primary={
|
<RdxPageActions
|
||||||
<Button onClick={() => navigate("/suppliers/create")}>
|
primary={
|
||||||
<PlusIcon aria-hidden className="mr-2 size-4" />
|
<Button onClick={() => navigate("/suppliers/create")}>
|
||||||
{t("common.actions.create")}
|
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||||
</Button>
|
{t("common.actions.create")}
|
||||||
}
|
</Button>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
description={t("list.description")}
|
}
|
||||||
title={t("list.title")}
|
description={t("list.description")}
|
||||||
/>
|
title={t("list.title")}
|
||||||
<SuppliersGrid
|
/>
|
||||||
columns={columns}
|
<SuppliersGrid
|
||||||
columnVisibility={controller.tablePreferences.columnVisibility}
|
columns={columns}
|
||||||
data={controller.data}
|
columnVisibility={controller.tablePreferences.columnVisibility}
|
||||||
fetching={controller.isFetching}
|
data={controller.data}
|
||||||
loading={controller.isLoading}
|
fetching={controller.isFetching}
|
||||||
onColumnVisibilityChange={controller.tablePreferences.setColumnVisibility}
|
loading={controller.isLoading}
|
||||||
onPageChange={controller.setPageIndex}
|
onColumnVisibilityChange={controller.tablePreferences.setColumnVisibility}
|
||||||
onPageSizeChange={controller.setPageSize}
|
onPageChange={controller.setPageIndex}
|
||||||
onSortChange={controller.setSort}
|
onPageSizeChange={controller.setPageSize}
|
||||||
pageIndex={controller.pageIndex}
|
onSortChange={controller.setSort}
|
||||||
pageSize={controller.pageSize}
|
pageIndex={controller.pageIndex}
|
||||||
searching={isSearchPending}
|
pageSize={controller.pageSize}
|
||||||
sort={controller.sort}
|
searching={isSearchPending}
|
||||||
topContent={
|
sort={controller.sort}
|
||||||
<SimpleSearchInput
|
topContent={
|
||||||
loading={controller.isFetching}
|
<SimpleSearchInput
|
||||||
onPendingChange={setIsSearchPending}
|
loading={controller.isFetching}
|
||||||
onSearchChange={controller.setSearch}
|
onPendingChange={setIsSearchPending}
|
||||||
value={controller.search}
|
onSearchChange={controller.setSearch}
|
||||||
/>
|
value={controller.search}
|
||||||
}
|
/>
|
||||||
/>
|
}
|
||||||
</RdxPageStack>
|
/>
|
||||||
</RdxPageContainer>
|
</RdxPageStack>
|
||||||
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
export * from "./supplier-editor-form";
|
export * from "./supplier-editor-form";
|
||||||
export * from "./supplier-editor-header";
|
export * from "./supplier-editor-header";
|
||||||
export * from "./supplier-editor-skeleton";
|
export * from "./supplier-editor-skeleton";
|
||||||
export * from "./supplier-layout";
|
|
||||||
export * from "./supplier-status-badge";
|
export * from "./supplier-status-badge";
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import { PageFormHeader, PageKeyboardShortcutsButton } from "@erp/core/components";
|
import { PageKeyboardShortcutsButton } from "@erp/core/components";
|
||||||
import { CancelActionButton, FormActionsBar, RhfSubmitActionButton } from "@repo/rdx-ui/components";
|
import {
|
||||||
|
CancelActionButton,
|
||||||
|
FormActionsBar,
|
||||||
|
RdxEditorHeader,
|
||||||
|
RhfSubmitActionButton,
|
||||||
|
} from "@repo/rdx-ui/components";
|
||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
@ -19,7 +24,7 @@ export const SupplierEditorHeader = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageFormHeader
|
<RdxEditorHeader
|
||||||
actions={
|
actions={
|
||||||
<FormActionsBar align="end" reverseOnMobile={false}>
|
<FormActionsBar align="end" reverseOnMobile={false}>
|
||||||
<PageKeyboardShortcutsButton
|
<PageKeyboardShortcutsButton
|
||||||
@ -49,7 +54,7 @@ export const SupplierEditorHeader = ({
|
|||||||
backLabel={t("common.actions.back")}
|
backLabel={t("common.actions.back")}
|
||||||
onBack={onCancel}
|
onBack={onCancel}
|
||||||
showStatus={hasChanges}
|
showStatus={hasChanges}
|
||||||
statusLabel={t("form.modified")}
|
status={t("form.modified")}
|
||||||
title={title}
|
title={title}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { AppContent } from "@repo/rdx-ui/components";
|
import { RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
|
|
||||||
@ -17,16 +17,18 @@ export const SupplierEditorSkeleton = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContent>
|
<RdxPageFrame>
|
||||||
<div aria-hidden className="space-y-6">
|
<RdxPageContainer width="content">
|
||||||
<div className="h-10 w-64 animate-pulse rounded-md bg-muted" />
|
<div aria-hidden className="space-y-6">
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="h-10 w-64 animate-pulse rounded-md bg-muted" />
|
||||||
{SKELETON_FIELDS.map((field) => (
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="h-16 animate-pulse rounded-md bg-muted" key={field} />
|
{SKELETON_FIELDS.map((field) => (
|
||||||
))}
|
<div className="h-16 animate-pulse rounded-md bg-muted" key={field} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span className="sr-only">{t("common.loading")}</span>
|
||||||
<span className="sr-only">{t("common.loading")}</span>
|
</RdxPageContainer>
|
||||||
</AppContent>
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
|
||||||
|
|
||||||
export const SupplierLayout = ({ children }: PropsWithChildren) => (
|
|
||||||
<div className="flex h-full w-full flex-col" id="suppliers">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
@ -2,9 +2,6 @@ import type { ModuleClientParams } from "@erp/core/client";
|
|||||||
import { lazy } from "react";
|
import { lazy } from "react";
|
||||||
import { Outlet, type RouteObject } from "react-router-dom";
|
import { Outlet, type RouteObject } from "react-router-dom";
|
||||||
|
|
||||||
const SupplierLayout = lazy(() =>
|
|
||||||
import("./shared/ui").then((module) => ({ default: module.SupplierLayout }))
|
|
||||||
);
|
|
||||||
const SuppliersListPage = lazy(() =>
|
const SuppliersListPage = lazy(() =>
|
||||||
import("./list").then((module) => ({ default: module.SuppliersListPage }))
|
import("./list").then((module) => ({ default: module.SuppliersListPage }))
|
||||||
);
|
);
|
||||||
@ -22,38 +19,22 @@ export const SupplierRoutes = (params: ModuleClientParams): RouteObject[] => [
|
|||||||
{
|
{
|
||||||
path: "suppliers",
|
path: "suppliers",
|
||||||
handle: { layout: "app-sidebar", protected: true },
|
handle: { layout: "app-sidebar", protected: true },
|
||||||
element: (
|
element: <Outlet context={params} />,
|
||||||
<SupplierLayout>
|
|
||||||
<Outlet context={params} />
|
|
||||||
</SupplierLayout>
|
|
||||||
),
|
|
||||||
children: [{ index: true, element: <SuppliersListPage /> }],
|
children: [{ index: true, element: <SuppliersListPage /> }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "suppliers/create",
|
path: "suppliers/create",
|
||||||
handle: { layout: "app-fullscreen", protected: true },
|
handle: { layout: "app-fullscreen", protected: true },
|
||||||
element: (
|
element: <SupplierCreatePage />,
|
||||||
<SupplierLayout>
|
|
||||||
<SupplierCreatePage />
|
|
||||||
</SupplierLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "suppliers/:id/edit",
|
path: "suppliers/:id/edit",
|
||||||
handle: { layout: "app-fullscreen", protected: true },
|
handle: { layout: "app-fullscreen", protected: true },
|
||||||
element: (
|
element: <SupplierUpdatePage />,
|
||||||
<SupplierLayout>
|
|
||||||
<SupplierUpdatePage />
|
|
||||||
</SupplierLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "suppliers/:id",
|
path: "suppliers/:id",
|
||||||
handle: { layout: "app-fullscreen", protected: true },
|
handle: { layout: "app-fullscreen", protected: true },
|
||||||
element: (
|
element: <SupplierDetailPage />,
|
||||||
<SupplierLayout>
|
|
||||||
<SupplierDetailPage />
|
|
||||||
</SupplierLayout>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ErrorAlert, PageErrorState } from "@erp/core/components";
|
import { ErrorAlert, PageErrorState } from "@erp/core/components";
|
||||||
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
import { UnsavedChangesProvider, useReturnToNavigation } from "@erp/core/hooks";
|
||||||
import { RdxEditorPageLayout, RdxPageContainer } from "@repo/rdx-ui/components";
|
import { RdxEditorLayout, RdxPageContainer, RdxPageFrame } from "@repo/rdx-ui/components";
|
||||||
import { FormProvider } from "react-hook-form";
|
import { FormProvider } from "react-hook-form";
|
||||||
import { createSearchParams, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
import { createSearchParams, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
@ -28,36 +28,42 @@ export const SupplierUpdatePage = () => {
|
|||||||
|
|
||||||
if (controller.query.isError || !controller.query.data) {
|
if (controller.query.isError || !controller.query.data) {
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<PageErrorState
|
<RdxPageContainer width="content">
|
||||||
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 controller.query.refetch();
|
onClick: async () => {
|
||||||
},
|
await controller.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"
|
||||||
</RdxPageContainer>
|
/>
|
||||||
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex flex-col overflow-hidden">
|
<FormProvider {...controller.form}>
|
||||||
<FormProvider {...controller.form}>
|
<UnsavedChangesProvider isDirty={controller.form.formState.isDirty}>
|
||||||
<UnsavedChangesProvider isDirty={controller.form.formState.isDirty}>
|
<RdxPageFrame
|
||||||
<SupplierEditorHeader
|
contentClassName="bg-muted/20"
|
||||||
formId={controller.formId}
|
header={
|
||||||
hasChanges={controller.form.formState.isDirty}
|
<SupplierEditorHeader
|
||||||
isSaving={controller.mutation.isPending}
|
formId={controller.formId}
|
||||||
onCancel={navigateBack}
|
hasChanges={controller.form.formState.isDirty}
|
||||||
title={t("update.title")}
|
isSaving={controller.mutation.isPending}
|
||||||
/>
|
onCancel={navigateBack}
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/20">
|
title={t("update.title")}
|
||||||
<RdxEditorPageLayout width="simple">
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RdxPageContainer width="content">
|
||||||
|
<RdxEditorLayout>
|
||||||
<div className="rounded-2xl border border-border/60 bg-background p-4 shadow-xs md:p-6">
|
<div className="rounded-2xl border border-border/60 bg-background p-4 shadow-xs md:p-6">
|
||||||
<div className="flex min-w-0 flex-col gap-6">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
{controller.mutation.isError ? (
|
{controller.mutation.isError ? (
|
||||||
@ -73,10 +79,10 @@ export const SupplierUpdatePage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RdxEditorPageLayout>
|
</RdxEditorLayout>
|
||||||
</div>
|
</RdxPageContainer>
|
||||||
</UnsavedChangesProvider>
|
</RdxPageFrame>
|
||||||
</FormProvider>
|
</UnsavedChangesProvider>
|
||||||
</div>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useReturnToNavigation } from "@erp/core/hooks";
|
|||||||
import {
|
import {
|
||||||
RdxPageActions,
|
RdxPageActions,
|
||||||
RdxPageContainer,
|
RdxPageContainer,
|
||||||
|
RdxPageFrame,
|
||||||
RdxPageHeader,
|
RdxPageHeader,
|
||||||
RdxPageStack,
|
RdxPageStack,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
@ -35,20 +36,22 @@ export const SupplierDetailPage = () => {
|
|||||||
|
|
||||||
if (query.isError || !query.data) {
|
if (query.isError || !query.data) {
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame>
|
||||||
<PageErrorState
|
<RdxPageContainer width="content">
|
||||||
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"
|
||||||
</RdxPageContainer>
|
/>
|
||||||
|
</RdxPageContainer>
|
||||||
|
</RdxPageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,121 +59,133 @@ export const SupplierDetailPage = () => {
|
|||||||
const context = supplier.reference || supplier.tin || supplier.tradeName || "-";
|
const context = supplier.reference || supplier.tin || supplier.tradeName || "-";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RdxPageContainer width="full">
|
<RdxPageFrame
|
||||||
<RdxPageStack spacing="default">
|
header={
|
||||||
<RdxPageHeader
|
<RdxPageContainer width="full">
|
||||||
actions={
|
<RdxPageHeader
|
||||||
<RdxPageActions
|
actions={
|
||||||
primary={
|
<RdxPageActions
|
||||||
<Button
|
primary={
|
||||||
onClick={() =>
|
<Button
|
||||||
navigate({
|
onClick={() =>
|
||||||
pathname: `/suppliers/${supplier.id}/edit`,
|
navigate({
|
||||||
search: createSearchParams({ returnTo }).toString(),
|
pathname: `/suppliers/${supplier.id}/edit`,
|
||||||
})
|
search: createSearchParams({ returnTo }).toString(),
|
||||||
}
|
})
|
||||||
>
|
}
|
||||||
<PencilIcon aria-hidden className="mr-2 size-4" />
|
>
|
||||||
{t("common.actions.edit")}
|
<PencilIcon aria-hidden className="mr-2 size-4" />
|
||||||
</Button>
|
{t("common.actions.edit")}
|
||||||
}
|
</Button>
|
||||||
secondary={
|
}
|
||||||
<Button onClick={navigateBack} variant="outline">
|
secondary={
|
||||||
{t("common.actions.back")}
|
<Button onClick={navigateBack} variant="outline">
|
||||||
</Button>
|
{t("common.actions.back")}
|
||||||
}
|
</Button>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
description={context}
|
}
|
||||||
title={
|
description={context}
|
||||||
<span className="flex flex-wrap items-center gap-3">
|
title={
|
||||||
{supplier.name}
|
<span className="flex flex-wrap items-center gap-3">
|
||||||
<SupplierStatusBadge status={supplier.status} />
|
{supplier.name}
|
||||||
</span>
|
<SupplierStatusBadge status={supplier.status} />
|
||||||
}
|
</span>
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
</RdxPageContainer>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RdxPageContainer width="full">
|
||||||
|
<RdxPageStack spacing="default">
|
||||||
|
<div className="grid gap-4 lg:grid-cols-2">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
|
<FileTextIcon aria-hidden className="size-5 text-primary" />
|
||||||
|
{t("detail.sections.identity")}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<dl className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<DetailField label={t("fields.name")} value={supplier.name} />
|
||||||
|
<DetailField label={t("fields.trade_name")} value={supplier.tradeName} />
|
||||||
|
<DetailField label={t("fields.reference")} value={supplier.reference} />
|
||||||
|
<DetailField label={t("fields.tin")} value={supplier.tin} />
|
||||||
|
<DetailField
|
||||||
|
label={t("fields.supplier_type")}
|
||||||
|
value={t(supplier.isCompany ? "fields.company" : "fields.individual")}
|
||||||
|
/>
|
||||||
|
</dl>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
<Card>
|
||||||
<Card>
|
<CardHeader>
|
||||||
<CardHeader>
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
<CardTitle className="flex items-center gap-2 text-lg">
|
<MailIcon aria-hidden className="size-5 text-primary" />
|
||||||
<FileTextIcon aria-hidden className="size-5 text-primary" />
|
{t("detail.sections.contact")}
|
||||||
{t("detail.sections.identity")}
|
</CardTitle>
|
||||||
</CardTitle>
|
</CardHeader>
|
||||||
</CardHeader>
|
<CardContent>
|
||||||
<CardContent>
|
<dl className="grid gap-4 sm:grid-cols-2">
|
||||||
<dl className="grid gap-4 sm:grid-cols-2">
|
<DetailField label={t("fields.primary_email")} value={supplier.primaryEmail} />
|
||||||
<DetailField label={t("fields.name")} value={supplier.name} />
|
<DetailField
|
||||||
<DetailField label={t("fields.trade_name")} value={supplier.tradeName} />
|
label={t("fields.secondary_email")}
|
||||||
<DetailField label={t("fields.reference")} value={supplier.reference} />
|
value={supplier.secondaryEmail}
|
||||||
<DetailField label={t("fields.tin")} value={supplier.tin} />
|
/>
|
||||||
<DetailField
|
<DetailField label={t("fields.primary_phone")} value={supplier.primaryPhone} />
|
||||||
label={t("fields.supplier_type")}
|
<DetailField
|
||||||
value={t(supplier.isCompany ? "fields.company" : "fields.individual")}
|
label={t("fields.secondary_phone")}
|
||||||
/>
|
value={supplier.secondaryPhone}
|
||||||
</dl>
|
/>
|
||||||
</CardContent>
|
<DetailField label={t("fields.primary_mobile")} value={supplier.primaryMobile} />
|
||||||
</Card>
|
<DetailField
|
||||||
|
label={t("fields.secondary_mobile")}
|
||||||
|
value={supplier.secondaryMobile}
|
||||||
|
/>
|
||||||
|
<DetailField label={t("fields.fax")} value={supplier.fax} />
|
||||||
|
<DetailField label={t("fields.website")} value={supplier.website} />
|
||||||
|
</dl>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-lg">
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
<MailIcon aria-hidden className="size-5 text-primary" />
|
<MapPinIcon aria-hidden className="size-5 text-primary" />
|
||||||
{t("detail.sections.contact")}
|
{t("detail.sections.address")}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<dl className="grid gap-4 sm:grid-cols-2">
|
<dl className="grid gap-4 sm:grid-cols-2">
|
||||||
<DetailField label={t("fields.primary_email")} value={supplier.primaryEmail} />
|
<DetailField label={t("fields.street")} value={supplier.street} />
|
||||||
<DetailField label={t("fields.secondary_email")} value={supplier.secondaryEmail} />
|
<DetailField label={t("fields.street2")} value={supplier.street2} />
|
||||||
<DetailField label={t("fields.primary_phone")} value={supplier.primaryPhone} />
|
<DetailField label={t("fields.city")} value={supplier.city} />
|
||||||
<DetailField label={t("fields.secondary_phone")} value={supplier.secondaryPhone} />
|
<DetailField label={t("fields.postal_code")} value={supplier.postalCode} />
|
||||||
<DetailField label={t("fields.primary_mobile")} value={supplier.primaryMobile} />
|
<DetailField label={t("fields.province")} value={supplier.province} />
|
||||||
<DetailField
|
<DetailField label={t("fields.country")} value={supplier.country} />
|
||||||
label={t("fields.secondary_mobile")}
|
</dl>
|
||||||
value={supplier.secondaryMobile}
|
</CardContent>
|
||||||
/>
|
</Card>
|
||||||
<DetailField label={t("fields.fax")} value={supplier.fax} />
|
|
||||||
<DetailField label={t("fields.website")} value={supplier.website} />
|
|
||||||
</dl>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2 text-lg">
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
<MapPinIcon aria-hidden className="size-5 text-primary" />
|
<LanguagesIcon aria-hidden className="size-5 text-primary" />
|
||||||
{t("detail.sections.address")}
|
{t("detail.sections.preferences")}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<dl className="grid gap-4 sm:grid-cols-2">
|
<dl className="grid gap-4 sm:grid-cols-2">
|
||||||
<DetailField label={t("fields.street")} value={supplier.street} />
|
<DetailField label={t("fields.language")} value={supplier.languageCode} />
|
||||||
<DetailField label={t("fields.street2")} value={supplier.street2} />
|
<DetailField label={t("fields.currency")} value={supplier.currencyCode} />
|
||||||
<DetailField label={t("fields.city")} value={supplier.city} />
|
</dl>
|
||||||
<DetailField label={t("fields.postal_code")} value={supplier.postalCode} />
|
</CardContent>
|
||||||
<DetailField label={t("fields.province")} value={supplier.province} />
|
</Card>
|
||||||
<DetailField label={t("fields.country")} value={supplier.country} />
|
</div>
|
||||||
</dl>
|
</RdxPageStack>
|
||||||
</CardContent>
|
</RdxPageContainer>
|
||||||
</Card>
|
</RdxPageFrame>
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2 text-lg">
|
|
||||||
<LanguagesIcon aria-hidden className="size-5 text-primary" />
|
|
||||||
{t("detail.sections.preferences")}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<dl className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<DetailField label={t("fields.language")} value={supplier.languageCode} />
|
|
||||||
<DetailField label={t("fields.currency")} value={supplier.currencyCode} />
|
|
||||||
</dl>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</RdxPageStack>
|
|
||||||
</RdxPageContainer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./rdx-editor-action-bar.tsx";
|
export * from "./rdx-editor-action-bar.tsx";
|
||||||
export * from "./rdx-editor-page-layout.tsx";
|
export * from "./rdx-editor-header.tsx";
|
||||||
|
export * from "./rdx-editor-layout.tsx";
|
||||||
export * from "./rdx-panel.tsx";
|
export * from "./rdx-panel.tsx";
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export interface RdxEditorActionBarProps extends Omit<ComponentPropsWithoutRef<"
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Barra inferior de acciones de una pantalla de edición.
|
* Barra inferior de acciones de un editor.
|
||||||
*
|
*
|
||||||
* Puede mostrar:
|
* Puede mostrar:
|
||||||
* - contenido auxiliar a la izquierda
|
* - contenido auxiliar a la izquierda
|
||||||
|
|||||||
@ -6,47 +6,49 @@ import { renderToStaticMarkup } from "react-dom/server";
|
|||||||
import { RdxFormGrid } from "../form/rdx-form-grid.tsx";
|
import { RdxFormGrid } from "../form/rdx-form-grid.tsx";
|
||||||
|
|
||||||
import { RdxEditorActionBar } from "./rdx-editor-action-bar.tsx";
|
import { RdxEditorActionBar } from "./rdx-editor-action-bar.tsx";
|
||||||
import { RdxEditorPageLayout } from "./rdx-editor-page-layout.tsx";
|
import { RdxEditorHeader } from "./rdx-editor-header.tsx";
|
||||||
|
import { RdxEditorLayout } from "./rdx-editor-layout.tsx";
|
||||||
|
|
||||||
|
test("RdxEditorLayout only distributes main content and an optional aside", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<RdxEditorLayout aside={<span>Aside</span>}>Main</RdxEditorLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(html, /grid/);
|
||||||
|
assert.match(html, /<aside/);
|
||||||
|
assert.doesNotMatch(html, /overflow/);
|
||||||
|
assert.doesNotMatch(html, /px-4/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("RdxEditorHeader renderiza navegación, título, estado, acciones y contenido contextual", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<RdxEditorHeader
|
||||||
|
actions={<span>Actions</span>}
|
||||||
|
backLabel="Volver"
|
||||||
|
onBack={() => undefined}
|
||||||
|
status="Modificada"
|
||||||
|
title="Editar proforma"
|
||||||
|
>
|
||||||
|
<div>Secondary content</div>
|
||||||
|
</RdxEditorHeader>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(html, /aria-label="Volver"/);
|
||||||
|
assert.match(html, /<h1[^>]*>Editar proforma<\/h1>/);
|
||||||
|
assert.match(html, /Modificada/);
|
||||||
|
assert.match(html, /Actions/);
|
||||||
|
assert.match(html, /Secondary content/);
|
||||||
|
assert.doesNotMatch(html, /sticky top-0/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("RdxEditorHeader no renderiza back sin callback", () => {
|
||||||
|
const html = renderToStaticMarkup(<RdxEditorHeader backLabel="Volver" title="Detalle" />);
|
||||||
|
|
||||||
|
assert.doesNotMatch(html, /aria-label="Volver"/);
|
||||||
|
});
|
||||||
|
|
||||||
import { RdxPanel } from "./rdx-panel.tsx";
|
import { RdxPanel } from "./rdx-panel.tsx";
|
||||||
|
|
||||||
test("RdxEditorPageLayout aplica complejidad simple por defecto y renderiza children", () => {
|
|
||||||
const html = renderToStaticMarkup(
|
|
||||||
<RdxEditorPageLayout>
|
|
||||||
<div>Editor content</div>
|
|
||||||
</RdxEditorPageLayout>
|
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
|
||||||
<RdxEditorPageLayout aside={<div>Aside content</div>} width="full">
|
|
||||||
<div>Main content</div>
|
|
||||||
</RdxEditorPageLayout>
|
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
|
||||||
<RdxEditorPageLayout width="full">
|
|
||||||
<div>Main content</div>
|
|
||||||
</RdxEditorPageLayout>
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.match(html, /Main content/);
|
|
||||||
assert.doesNotMatch(html, /<aside/);
|
|
||||||
assert.doesNotMatch(html, /grid-cols-\[/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("RdxPanel plain renderiza header semántico y contenido sin card", () => {
|
test("RdxPanel plain renderiza header semántico y contenido sin card", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<RdxPanel
|
<RdxPanel
|
||||||
|
|||||||
@ -1,74 +1,75 @@
|
|||||||
import { RdxBadge } from "@repo/rdx-ui/components";
|
|
||||||
import { Button } from "@repo/shadcn-ui/components";
|
import { Button } from "@repo/shadcn-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { ArrowLeftIcon } from "lucide-react";
|
import { ArrowLeftIcon } from "lucide-react";
|
||||||
import type { ReactNode } from "react";
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||||
|
|
||||||
export interface PageFormHeaderProps {
|
import { RdxBadge } from "../rdx-badge.tsx";
|
||||||
title: string | ReactNode;
|
|
||||||
|
|
||||||
backLabel: string;
|
|
||||||
onBack?: () => void;
|
|
||||||
|
|
||||||
statusLabel?: string;
|
|
||||||
showStatus?: boolean;
|
|
||||||
|
|
||||||
|
export interface RdxEditorHeaderProps
|
||||||
|
extends Omit<ComponentPropsWithoutRef<"header">, "children" | "title"> {
|
||||||
|
title: ReactNode;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
|
backLabel?: string;
|
||||||
|
onBack?: () => void;
|
||||||
|
status?: ReactNode;
|
||||||
|
showStatus?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
|
||||||
className?: string;
|
|
||||||
contentClassName?: string;
|
contentClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageFormHeader = ({
|
/**
|
||||||
|
* Cabecera compacta para páginas fullscreen.
|
||||||
|
* Presenta navegación de retorno, título, estado y acciones.
|
||||||
|
*/
|
||||||
|
export const RdxEditorHeader = ({
|
||||||
title,
|
title,
|
||||||
|
actions,
|
||||||
backLabel,
|
backLabel,
|
||||||
onBack,
|
onBack,
|
||||||
statusLabel,
|
status,
|
||||||
showStatus = false,
|
showStatus = true,
|
||||||
actions,
|
|
||||||
children,
|
children,
|
||||||
className,
|
|
||||||
contentClassName,
|
contentClassName,
|
||||||
}: PageFormHeaderProps) => {
|
className,
|
||||||
|
...props
|
||||||
|
}: RdxEditorHeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn("z-50 shrink-0 bg-background ring-1 ring-foreground/10 shadow-xs", className)}
|
||||||
"sticky top-0 z-50 ring-1 ring-foreground/10 bg-background shadow-xs",
|
{...props}
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-14 items-center justify-between gap-6 border-b px-4 py-2 sm:px-6 md:h-18",
|
"flex min-h-14 items-center justify-between gap-6 border-b px-4 py-2 sm:px-6 md:min-h-18",
|
||||||
contentClassName
|
contentClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
<Button
|
{onBack ? (
|
||||||
aria-label={backLabel}
|
<Button
|
||||||
disabled={!onBack}
|
aria-label={backLabel}
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon aria-hidden="true" className="size-5" />
|
<ArrowLeftIcon aria-hidden="true" className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="flex min-w-0 items-center gap-4">
|
<div className="flex min-w-0 items-center gap-4">
|
||||||
<h1 className="min-w-0 truncate text-xl font-semibold tracking-tight text-foreground lg:text-2xl">
|
<h1 className="min-w-0 truncate text-xl font-semibold tracking-tight text-foreground lg:text-2xl">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{statusLabel ? (
|
{status ? (
|
||||||
<RdxBadge
|
<RdxBadge
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-amber-500 bg-amber-200 text-amber-900 transition-opacity duration-200",
|
"border-amber-500 bg-amber-200 text-amber-900 transition-opacity duration-200",
|
||||||
showStatus ? "opacity-100" : "pointer-events-none opacity-0"
|
showStatus ? "opacity-100" : "pointer-events-none opacity-0"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{statusLabel}
|
{status}
|
||||||
</RdxBadge>
|
</RdxBadge>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||||
|
|
||||||
|
export interface RdxEditorLayoutProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
||||||
|
children: ReactNode;
|
||||||
|
aside?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distribuye el contenido interno de un editor entre una región principal
|
||||||
|
* y un aside opcional.
|
||||||
|
*/
|
||||||
|
export const RdxEditorLayout = ({ children, aside, className, ...props }: RdxEditorLayoutProps) => {
|
||||||
|
if (!aside) {
|
||||||
|
return (
|
||||||
|
<div className={cn("min-w-0", className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("grid min-w-0 gap-6 xl:grid-cols-[minmax(0,1fr)_22rem]", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="min-w-0">{children}</div>
|
||||||
|
<aside className="min-w-0">{aside}</aside>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
|
||||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
||||||
|
|
||||||
export type RdxEditorWidth = "wide" | "full";
|
|
||||||
|
|
||||||
export interface RdxEditorPageLayoutProps
|
|
||||||
extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
||||||
children: ReactNode;
|
|
||||||
aside?: ReactNode;
|
|
||||||
width?: RdxEditorWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CONTENT_WIDTH_CLASSNAME: Record<RdxEditorWidth, string> = {
|
|
||||||
wide: "mx-auto w-full max-w-5xl",
|
|
||||||
full: "w-full",
|
|
||||||
};
|
|
||||||
|
|
||||||
const ASIDE_GRID_CLASSNAME: Record<RdxEditorWidth, string> = {
|
|
||||||
wide: "xl:grid-cols-[minmax(0,1fr)_280px]",
|
|
||||||
full: "xl:grid-cols-[minmax(0,1fr)_22rem]",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Distribuye el contenido interno de un contenedor en:
|
|
||||||
*
|
|
||||||
* - una región principal
|
|
||||||
* - un aside opcional.
|
|
||||||
*
|
|
||||||
* No controla padding, scroll ni estructura general de página.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const RdxEditorPageLayout = ({
|
|
||||||
children,
|
|
||||||
aside,
|
|
||||||
width = "wide",
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: RdxEditorPageLayoutProps) => {
|
|
||||||
const contentClassName = CONTENT_WIDTH_CLASSNAME[width];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn("rdx-editor-page-layout min-w-0 w-full px-4 py-4 md:px-6 md:py-6", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{aside ? (
|
|
||||||
<div className={cn(contentClassName, "grid min-w-0 gap-6", ASIDE_GRID_CLASSNAME[width])}>
|
|
||||||
<div className="min-w-0">{children}</div>
|
|
||||||
<aside className="min-w-0">{aside}</aside>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className={cn("min-w-0", contentClassName)}>{children}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -43,7 +43,6 @@ export const RdxPanel = ({
|
|||||||
...props
|
...props
|
||||||
}: RdxPanelProps) => {
|
}: RdxPanelProps) => {
|
||||||
const titleId = useId();
|
const titleId = useId();
|
||||||
const hasHeader = Boolean(title || description || icon || actions);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
@ -51,34 +50,30 @@ export const RdxPanel = ({
|
|||||||
className={cn(VARIANT_CLASSNAME[variant], className)}
|
className={cn(VARIANT_CLASSNAME[variant], className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{hasHeader ? (
|
<div className={cn("flex flex-col gap-4", variant === "plain" ? "pb-4" : "pb-5")}>
|
||||||
<div className={cn("flex flex-col gap-4", variant === "plain" ? "pb-4" : "pb-5")}>
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex min-w-0 items-start gap-3">
|
||||||
<div className="flex min-w-0 items-start gap-3">
|
{icon ? (
|
||||||
{icon ? (
|
<div className="flex size-6 shrink-0 items-center justify-center text-primary">
|
||||||
<div className="flex size-6 shrink-0 items-center justify-center text-primary">
|
{icon}
|
||||||
{icon}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="min-w-0 space-y-1">
|
|
||||||
<h2
|
|
||||||
className="min-w-0 text-base font-semibold tracking-tight text-foreground"
|
|
||||||
id={titleId}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{description ? (
|
|
||||||
<p className="text-sm text-muted-foreground">{description}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
|
|
||||||
{actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
|
<div className="min-w-0 space-y-1">
|
||||||
|
<h2
|
||||||
|
className="min-w-0 text-base font-semibold tracking-tight text-foreground"
|
||||||
|
id={titleId}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0">{children}</div>
|
<div className="min-w-0">{children}</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -3,5 +3,6 @@ export * from "./app-header.tsx";
|
|||||||
export * from "./app-layout.tsx";
|
export * from "./app-layout.tsx";
|
||||||
export * from "./rdx-page-actions.tsx";
|
export * from "./rdx-page-actions.tsx";
|
||||||
export * from "./rdx-page-container.tsx";
|
export * from "./rdx-page-container.tsx";
|
||||||
|
export * from "./rdx-page-frame.tsx";
|
||||||
export * from "./rdx-page-header.tsx";
|
export * from "./rdx-page-header.tsx";
|
||||||
export * from "./rdx-page-stack.tsx";
|
export * from "./rdx-page-stack.tsx";
|
||||||
|
|||||||
@ -1,12 +1,27 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
|
||||||
import { RdxPageActions } from "./rdx-page-actions.tsx";
|
import { RdxPageActions } from "./rdx-page-actions.tsx";
|
||||||
import { RdxPageContainer } from "./rdx-page-container.tsx";
|
import { RdxPageContainer } from "./rdx-page-container.tsx";
|
||||||
|
import { RdxPageFrame } from "./rdx-page-frame.tsx";
|
||||||
import { RdxPageHeader } from "./rdx-page-header.tsx";
|
import { RdxPageHeader } from "./rdx-page-header.tsx";
|
||||||
import { RdxPageStack } from "./rdx-page-stack.tsx";
|
import { RdxPageStack } from "./rdx-page-stack.tsx";
|
||||||
|
|
||||||
|
test("RdxPageFrame separates fixed regions from the scrollable content", () => {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<RdxPageFrame footer={<span>Footer</span>} header={<span>Header</span>}>
|
||||||
|
Content
|
||||||
|
</RdxPageFrame>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(html, /overflow-hidden/);
|
||||||
|
assert.match(html, /overflow-y-auto/);
|
||||||
|
assert.ok(html.indexOf("Header") < html.indexOf("Content"));
|
||||||
|
assert.ok(html.indexOf("Content") < html.indexOf("Footer"));
|
||||||
|
});
|
||||||
|
|
||||||
test("RdxPageContainer renders children with full width by default", () => {
|
test("RdxPageContainer renders children with full width by default", () => {
|
||||||
const html = renderToStaticMarkup(<RdxPageContainer>Content</RdxPageContainer>);
|
const html = renderToStaticMarkup(<RdxPageContainer>Content</RdxPageContainer>);
|
||||||
|
|
||||||
@ -91,29 +106,6 @@ test("RdxPageHeader omits optional wrappers when content is absent", () => {
|
|||||||
assert.doesNotMatch(html, /Metadata/);
|
assert.doesNotMatch(html, /Metadata/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("RdxPageHeader soporta el modo sticky de editor con navegación, estado y contenido secundario", () => {
|
|
||||||
const html = renderToStaticMarkup(
|
|
||||||
<RdxPageHeader
|
|
||||||
actions={<span>Actions</span>}
|
|
||||||
backLabel="Volver"
|
|
||||||
onBack={() => undefined}
|
|
||||||
showStatus
|
|
||||||
statusLabel="Modificada"
|
|
||||||
sticky
|
|
||||||
title="Editar proforma"
|
|
||||||
>
|
|
||||||
<div>Secondary content</div>
|
|
||||||
</RdxPageHeader>
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.match(html, /sticky top-0/);
|
|
||||||
assert.match(html, /aria-label="Volver"/);
|
|
||||||
assert.match(html, /Editar proforma/);
|
|
||||||
assert.match(html, /Modificada/);
|
|
||||||
assert.match(html, /Actions/);
|
|
||||||
assert.match(html, /Secondary content/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("RdxPageActions renders only populated regions in the expected order", () => {
|
test("RdxPageActions renders only populated regions in the expected order", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<RdxPageActions
|
<RdxPageActions
|
||||||
|
|||||||
35
packages/rdx-ui/src/components/layout/rdx-page-frame.tsx
Normal file
35
packages/rdx-ui/src/components/layout/rdx-page-frame.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||||
|
|
||||||
|
export interface RdxPageFrameProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
||||||
|
children: ReactNode;
|
||||||
|
header?: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
contentClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estructura una página en cabecera, contenido scrollable y pie opcional.
|
||||||
|
* Centraliza el control del scroll principal de la página.
|
||||||
|
*/
|
||||||
|
export const RdxPageFrame = ({
|
||||||
|
children,
|
||||||
|
header,
|
||||||
|
footer,
|
||||||
|
contentClassName,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: RdxPageFrameProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{header ? <div className="shrink-0">{header}</div> : null}
|
||||||
|
<div className={cn("min-h-0 min-w-0 flex-1 overflow-y-auto", contentClassName)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{footer ? <div className="shrink-0">{footer}</div> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,10 +1,6 @@
|
|||||||
import { Button } from "@repo/shadcn-ui/components";
|
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
import { ArrowLeftIcon } from "lucide-react";
|
|
||||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||||
|
|
||||||
import { RdxBadge } from "../rdx-badge.tsx";
|
|
||||||
|
|
||||||
export interface RdxPageHeaderProps
|
export interface RdxPageHeaderProps
|
||||||
extends Omit<ComponentPropsWithoutRef<"header">, "children" | "title"> {
|
extends Omit<ComponentPropsWithoutRef<"header">, "children" | "title"> {
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
@ -12,27 +8,13 @@ export interface RdxPageHeaderProps
|
|||||||
leading?: ReactNode;
|
leading?: ReactNode;
|
||||||
metadata?: ReactNode;
|
metadata?: ReactNode;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
backLabel?: string;
|
|
||||||
onBack?: () => void;
|
|
||||||
statusLabel?: ReactNode;
|
|
||||||
showStatus?: boolean;
|
|
||||||
sticky?: boolean;
|
|
||||||
contentClassName?: string;
|
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cabecera estándar de una página.
|
* Cabecera estándar de una página.
|
||||||
*
|
*
|
||||||
* Muestra:
|
* Presenta título, descripción, metadata y acciones.
|
||||||
* - navegación de retorno opcional
|
|
||||||
* - título
|
|
||||||
* - descripción o metadata opcional
|
|
||||||
* - estado opcional
|
|
||||||
* - acciones de página
|
|
||||||
*
|
|
||||||
* Puede permanecer fija dentro de un contenedor scrollable
|
|
||||||
* mediante `sticky`.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const RdxPageHeader = ({
|
export const RdxPageHeader = ({
|
||||||
@ -41,73 +23,10 @@ export const RdxPageHeader = ({
|
|||||||
leading,
|
leading,
|
||||||
metadata,
|
metadata,
|
||||||
actions,
|
actions,
|
||||||
backLabel,
|
|
||||||
onBack,
|
|
||||||
statusLabel,
|
|
||||||
showStatus = false,
|
|
||||||
sticky = false,
|
|
||||||
contentClassName,
|
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: RdxPageHeaderProps) => {
|
}: RdxPageHeaderProps) => {
|
||||||
if (sticky) {
|
|
||||||
const shouldRenderBackButton = Boolean(backLabel || onBack);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header
|
|
||||||
className={cn(
|
|
||||||
"sticky top-0 z-50 shrink-0 bg-background ring-1 ring-foreground/10 shadow-xs",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex min-h-14 items-center justify-between gap-6 border-b px-4 py-2 sm:px-6 md:min-h-18",
|
|
||||||
contentClassName
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
|
||||||
{shouldRenderBackButton ? (
|
|
||||||
<Button
|
|
||||||
aria-label={backLabel}
|
|
||||||
disabled={!onBack}
|
|
||||||
onClick={onBack}
|
|
||||||
size="icon"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon aria-hidden="true" className="size-5" />
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="flex min-w-0 items-center gap-4">
|
|
||||||
<h1 className="min-w-0 truncate text-xl font-semibold tracking-tight text-foreground lg:text-2xl">
|
|
||||||
{title}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{statusLabel ? (
|
|
||||||
<RdxBadge
|
|
||||||
className={cn(
|
|
||||||
"border-amber-500 bg-amber-200 text-amber-900 transition-opacity duration-200",
|
|
||||||
showStatus ? "opacity-100" : "pointer-events-none opacity-0"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{statusLabel}
|
|
||||||
</RdxBadge>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{actions ? <div className="flex items-center gap-4">{actions}</div> : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@ -16,8 +16,8 @@ const SPACING_CLASSNAME: Record<RdxPageStackSpacing, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contenedor para layout para apilar contenido verticalmente.
|
* Apila contenido verticalmente aplicando un espaciado consistente.
|
||||||
* No proporciona estructura de página ni controla scroll.
|
* No controla viewport, scroll ni estructura de página.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const RdxPageStack = ({
|
export const RdxPageStack = ({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user