Pantalla update fullscreen
This commit is contained in:
parent
823e5cebe1
commit
66623cbe78
@ -1,57 +1,146 @@
|
||||
import type { IModuleClient, ModuleClientParams } from "@erp/core/client";
|
||||
import type { JSX } from "react";
|
||||
import { type RouteObject, useRoutes } from "react-router-dom";
|
||||
import type {
|
||||
IModuleClient,
|
||||
ModuleClientParams,
|
||||
ModuleRouteLayout,
|
||||
ModuleRouteObject,
|
||||
} from "@erp/core/client";
|
||||
import type { ReactNode } from "react";
|
||||
import type { RouteObject } from "react-router-dom";
|
||||
|
||||
interface ModuleRoutesProps {
|
||||
import { RequireAuthRouteGuard } from "@/routes/require-auth-guard";
|
||||
|
||||
interface BuildModuleRoutesParams {
|
||||
modules: IModuleClient[];
|
||||
params: ModuleClientParams;
|
||||
layout: ModuleRouteLayout;
|
||||
}
|
||||
|
||||
interface WarpIfProtectedProps {
|
||||
component: JSX.Element;
|
||||
isProtected: boolean;
|
||||
interface NormalizeRouteParams {
|
||||
route: ModuleRouteObject;
|
||||
module: IModuleClient;
|
||||
targetLayout: ModuleRouteLayout;
|
||||
inheritedLayout?: ModuleRouteLayout;
|
||||
inheritedProtected?: boolean;
|
||||
}
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedVariables: <posible uso futuro>
|
||||
const WarpIfProtected = ({ component, isProtected }: WarpIfProtectedProps) => {
|
||||
return isProtected ? <>{component}</> : component;
|
||||
const DEFAULT_LAYOUT: ModuleRouteLayout = "app-sidebar";
|
||||
|
||||
const resolveRouteLayout = (
|
||||
route: ModuleRouteObject,
|
||||
module: IModuleClient,
|
||||
inheritedLayout?: ModuleRouteLayout
|
||||
): ModuleRouteLayout => {
|
||||
return route.handle?.layout ?? inheritedLayout ?? module.layout ?? DEFAULT_LAYOUT;
|
||||
};
|
||||
|
||||
export const ModuleRoutes = ({ modules, params }: ModuleRoutesProps) => {
|
||||
const resolveRouteProtected = (
|
||||
route: ModuleRouteObject,
|
||||
module: IModuleClient,
|
||||
inheritedProtected?: boolean
|
||||
): boolean => {
|
||||
return route.handle?.protected ?? inheritedProtected ?? module.protected ?? false;
|
||||
};
|
||||
|
||||
const wrapProtectedElement = (element: ReactNode, isProtected: boolean): ReactNode => {
|
||||
if (!isProtected) {
|
||||
return element;
|
||||
}
|
||||
|
||||
return <RequireAuthRouteGuard>{element}</RequireAuthRouteGuard>;
|
||||
};
|
||||
|
||||
const normalizeRoute = ({
|
||||
route,
|
||||
module,
|
||||
targetLayout,
|
||||
inheritedLayout,
|
||||
inheritedProtected,
|
||||
}: NormalizeRouteParams): RouteObject | null => {
|
||||
const routeLayout = resolveRouteLayout(route, module, inheritedLayout);
|
||||
const routeProtected = resolveRouteProtected(route, module, inheritedProtected);
|
||||
|
||||
const normalizedChildren = route.children
|
||||
?.map((child) =>
|
||||
normalizeRoute({
|
||||
route: child,
|
||||
module,
|
||||
targetLayout,
|
||||
inheritedLayout: routeLayout,
|
||||
inheritedProtected: routeProtected,
|
||||
})
|
||||
)
|
||||
.filter((child): child is RouteObject => child !== null);
|
||||
|
||||
const hasMatchingLayout = routeLayout === targetLayout;
|
||||
const hasChildren = Boolean(normalizedChildren?.length);
|
||||
const hasElement = route.element !== undefined || route.Component !== undefined;
|
||||
|
||||
if (!(hasMatchingLayout || hasChildren)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hasMatchingLayout && hasChildren) {
|
||||
return {
|
||||
path: route.path,
|
||||
index: route.index,
|
||||
children: normalizedChildren,
|
||||
} as RouteObject;
|
||||
}
|
||||
|
||||
const { children: _children, handle, element, ...rest } = route;
|
||||
|
||||
const normalizedRoute = {
|
||||
...rest,
|
||||
handle,
|
||||
element: element ? wrapProtectedElement(element, routeProtected) : undefined,
|
||||
children: normalizedChildren,
|
||||
} as RouteObject;
|
||||
|
||||
if (!(hasElement || hasChildren)) {
|
||||
console.warn(
|
||||
`[ModuleRoutes] Ruta sin element ni children válidos: "${String(route.path ?? "index")}"`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizedRoute;
|
||||
};
|
||||
|
||||
export const buildModuleRoutes = ({
|
||||
modules,
|
||||
params,
|
||||
layout,
|
||||
}: BuildModuleRoutesParams): RouteObject[] => {
|
||||
const routes: RouteObject[] = [];
|
||||
|
||||
if (modules) {
|
||||
for (const module of modules) {
|
||||
if (typeof module.routes !== "function") {
|
||||
console.warn(`[ModuleRoutes] El módulo "${module.name}" no define una función 'routes()'`);
|
||||
continue;
|
||||
}
|
||||
for (const module of modules) {
|
||||
if (typeof module.routes !== "function") {
|
||||
console.warn(`[ModuleRoutes] El módulo "${module.name}" no define una función routes().`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const moduleRoutes = module.routes(params);
|
||||
const moduleRoutes = module.routes(params);
|
||||
|
||||
if (!Array.isArray(moduleRoutes)) {
|
||||
console.error(
|
||||
`[ModuleRoutes] El módulo "${module.name}" debe devolver un RouteObject[], pero devolvió:`,
|
||||
moduleRoutes
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const allAreRouteObjects = moduleRoutes.every(
|
||||
(r) => typeof r === "object" && r.element !== undefined
|
||||
if (!Array.isArray(moduleRoutes)) {
|
||||
console.error(
|
||||
`[ModuleRoutes] El módulo "${module.name}" debe devolver un ModuleRouteObject[], pero devolvió:`,
|
||||
moduleRoutes
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!allAreRouteObjects) {
|
||||
console.error(
|
||||
`[ModuleRoutes] El módulo "${module.name}" contiene elementos inválidos en su RouteObject[]`,
|
||||
moduleRoutes
|
||||
);
|
||||
continue;
|
||||
for (const route of moduleRoutes) {
|
||||
const normalizedRoute = normalizeRoute({
|
||||
route,
|
||||
module,
|
||||
targetLayout: layout,
|
||||
});
|
||||
|
||||
if (normalizedRoute) {
|
||||
routes.push(normalizedRoute);
|
||||
}
|
||||
|
||||
routes.push(...moduleRoutes);
|
||||
}
|
||||
}
|
||||
|
||||
return useRoutes(routes);
|
||||
return routes;
|
||||
};
|
||||
|
||||
9
apps/web/src/layout/app-fullscreen-layout.tsx
Normal file
9
apps/web/src/layout/app-fullscreen-layout.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
export const AppFullscreenLayout = () => {
|
||||
return (
|
||||
<main className="h-dvh w-dvw overflow-hidden bg-background">
|
||||
<Outlet />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@ -6,7 +6,7 @@ import { AppMain } from "./app-main";
|
||||
import { AppSidebar } from "./app-sidebar";
|
||||
import { AppTopbar } from "./app-topbar";
|
||||
|
||||
export const AppLayout = () => {
|
||||
export const AppSidebarLayout = () => {
|
||||
return (
|
||||
<SidebarProvider
|
||||
style={
|
||||
11
apps/web/src/layout/auth-layout.tsx
Normal file
11
apps/web/src/layout/auth-layout.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
export const AuthLayout = () => {
|
||||
return (
|
||||
<main className="grid min-h-dvh bg-muted px-4 py-8">
|
||||
<section className="m-auto w-full max-w-md">
|
||||
<Outlet />
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@ -1,4 +1,6 @@
|
||||
export * from './app-layout';
|
||||
export * from './app-main';
|
||||
export * from './app-sidebar';
|
||||
export * from './app-topbar';
|
||||
export * from "./app-fullscreen-layout";
|
||||
export * from "./app-main";
|
||||
export * from "./app-sidebar";
|
||||
export * from "./app-sidebar-layout";
|
||||
export * from "./app-topbar";
|
||||
export * from "./auth-layout";
|
||||
|
||||
@ -1,66 +1,103 @@
|
||||
// apps/web/src/routes/app-routes.tsx
|
||||
import type { IModuleClient } from "@erp/core/client";
|
||||
import { Navigate, Route, createBrowserRouter, createRoutesFromElements } from "react-router-dom";
|
||||
|
||||
import { ModuleRoutes } from "@/components/module-routes";
|
||||
import { AppLayout } from "@/layout";
|
||||
import { Navigate, createBrowserRouter } from "react-router-dom";
|
||||
|
||||
import { buildModuleRoutes } from "@/components/module-routes";
|
||||
import { AppFullscreenLayout, AppSidebarLayout, AuthLayout } from "@/layout";
|
||||
import ShadcnShowcasePage from "@/pages/shadcn-ui-page";
|
||||
import TailwindV4ShowcasePage from "@/pages/tailwindcss-page";
|
||||
|
||||
import { ErrorPage, LoginForm } from "../pages";
|
||||
import { ErrorPage } from "../pages";
|
||||
import { modules } from "../register-modules";
|
||||
|
||||
function groupModulesByLayout(modules: IModuleClient[]) {
|
||||
const groups: Record<string, IModuleClient[]> = {
|
||||
auth: [],
|
||||
app: [],
|
||||
};
|
||||
|
||||
for (const module of modules) {
|
||||
const layout = typeof module.layout === "string" ? module.layout : "app";
|
||||
|
||||
groups[layout] = groups[layout] ?? [];
|
||||
groups[layout].push(module);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
export const getAppRouter = () => {
|
||||
const params = {
|
||||
...import.meta.env,
|
||||
};
|
||||
|
||||
const grouped = groupModulesByLayout(modules);
|
||||
const authRoutes = buildModuleRoutes({
|
||||
modules,
|
||||
params,
|
||||
layout: "auth",
|
||||
});
|
||||
|
||||
return createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route path="/">
|
||||
<Route element={<Navigate replace to="/proformas" />} index />
|
||||
const appSidebarRoutes = buildModuleRoutes({
|
||||
modules,
|
||||
params,
|
||||
layout: "app-sidebar",
|
||||
});
|
||||
|
||||
{/* Auth Layout */}
|
||||
<Route path="auth">
|
||||
<Route element={<Navigate replace to="login" />} index />
|
||||
<Route element={<LoginForm />} path="login" />
|
||||
<Route element={<ModuleRoutes modules={grouped.auth} params={params} />} path="*" />
|
||||
</Route>
|
||||
const appFullscreenRoutes = buildModuleRoutes({
|
||||
modules,
|
||||
params,
|
||||
layout: "app-fullscreen",
|
||||
});
|
||||
|
||||
{/* App Layout */}
|
||||
<Route element={<AppLayout />}>
|
||||
{/* Test */}
|
||||
<Route element={<ShadcnShowcasePage />} path="shadcnui" />
|
||||
<Route element={<TailwindV4ShowcasePage />} path="tailwindcss4" />
|
||||
const appRoutes = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate replace to="/proformas" />,
|
||||
},
|
||||
|
||||
{/* Static / provisional routes */}
|
||||
<Route element={<ErrorPage />} path="dashboard" />
|
||||
<Route element={<ErrorPage />} path="settings" />
|
||||
<Route element={<ErrorPage />} path="catalog" />
|
||||
<Route element={<ErrorPage />} path="quotes" />
|
||||
{
|
||||
path: "auth",
|
||||
element: <AuthLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate replace to="login" />,
|
||||
},
|
||||
...authRoutes,
|
||||
],
|
||||
},
|
||||
|
||||
{/* Dynamic module routes. Keep this last. */}
|
||||
<Route element={<ModuleRoutes modules={grouped.app} params={params} />} path="*" />
|
||||
</Route>
|
||||
</Route>
|
||||
)
|
||||
);
|
||||
{
|
||||
element: <AppFullscreenLayout />,
|
||||
children: appFullscreenRoutes,
|
||||
},
|
||||
|
||||
{
|
||||
element: <AppSidebarLayout />,
|
||||
children: [
|
||||
{
|
||||
path: "shadcnui",
|
||||
element: <ShadcnShowcasePage />,
|
||||
},
|
||||
{
|
||||
path: "tailwindcss4",
|
||||
element: <TailwindV4ShowcasePage />,
|
||||
},
|
||||
{
|
||||
path: "dashboard",
|
||||
element: <ErrorPage />,
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
element: <ErrorPage />,
|
||||
},
|
||||
{
|
||||
path: "catalog",
|
||||
element: <ErrorPage />,
|
||||
},
|
||||
{
|
||||
path: "quotes",
|
||||
element: <ErrorPage />,
|
||||
},
|
||||
|
||||
...appSidebarRoutes,
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: "*",
|
||||
element: <ErrorPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
return appRoutes;
|
||||
};
|
||||
|
||||
18
apps/web/src/routes/public-only-guard.tsx
Normal file
18
apps/web/src/routes/public-only-guard.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
// apps/web/src/routes/guards/public-only-route.tsx
|
||||
import type { ReactNode } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
||||
interface PublicOnlyRouteProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const PublicOnlyRouteGuard = ({ children }: PublicOnlyRouteProps) => {
|
||||
// TODO: sustituir por tu estado real de auth.
|
||||
const isAuthenticated = false;
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Navigate replace to="/proformas" />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
20
apps/web/src/routes/require-auth-guard.tsx
Normal file
20
apps/web/src/routes/require-auth-guard.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
// apps/web/src/routes/guards/require-auth.tsx
|
||||
import type { ReactNode } from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
|
||||
interface RequireAuthRouteProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const RequireAuthRouteGuard = ({ children }: RequireAuthRouteProps) => {
|
||||
const location = useLocation();
|
||||
|
||||
// TODO: sustituir por tu estado real de auth.
|
||||
const isAuthenticated = true;
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate replace state={{ from: location }} to="/auth/login" />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@ -1,27 +1,33 @@
|
||||
import { ModuleClientParams } from "@erp/core/client";
|
||||
import { Outlet, RouteObject } from "react-router-dom";
|
||||
import { AuthLayout } from "./components";
|
||||
import type { ModuleClientParams } from "@erp/core/client";
|
||||
import type { RouteObject } from "react-router-dom";
|
||||
|
||||
import { LoginPage } from "./pages";
|
||||
|
||||
export const AuthRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||
return [
|
||||
{
|
||||
path: "*",
|
||||
element: (
|
||||
<AuthLayout>
|
||||
<Outlet context={params} />
|
||||
</AuthLayout>
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: "register",
|
||||
element: <div>Register</div>,
|
||||
},
|
||||
],
|
||||
path: "login",
|
||||
handle: {
|
||||
layout: "auth",
|
||||
protected: false,
|
||||
},
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: "register",
|
||||
handle: {
|
||||
layout: "auth",
|
||||
protected: false,
|
||||
},
|
||||
//element: <RegisterPage />,
|
||||
},
|
||||
{
|
||||
path: "forgot-password",
|
||||
handle: {
|
||||
layout: "auth",
|
||||
protected: false,
|
||||
},
|
||||
//element: <ForgotPasswordPage />,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import type { IModuleClient, ModuleClientParams } from "@erp/core/client";
|
||||
import type { IModuleClient } from "@erp/core/client";
|
||||
|
||||
import { AuthRoutes } from "./auth-routes";
|
||||
|
||||
//import enResources from "../common/locales/en.json";
|
||||
//import esResources from "../common/locales/es.json";
|
||||
|
||||
const MODULE_NAME = "auth";
|
||||
const MODULE_VERSION = "1.0.0";
|
||||
|
||||
@ -13,13 +10,10 @@ export const AuthModuleManifest: IModuleClient = {
|
||||
version: MODULE_VERSION,
|
||||
dependencies: ["core"],
|
||||
protected: false,
|
||||
|
||||
layout: "auth",
|
||||
|
||||
routes: (params: ModuleClientParams) => {
|
||||
//i18next.addResourceBundle("en", MODULE_NAME, enResources, true, true);
|
||||
//i18next.addResourceBundle("es", MODULE_NAME, esResources, true, true);
|
||||
return AuthRoutes(params);
|
||||
},
|
||||
routes: (params) => AuthRoutes(params),
|
||||
};
|
||||
|
||||
export default AuthModuleManifest;
|
||||
|
||||
@ -3,11 +3,34 @@ import type { RouteObject } from "react-router-dom";
|
||||
|
||||
import type { ModuleMetadata } from "../../../common";
|
||||
|
||||
export type ModuleClientParams = { [key: string]: any };
|
||||
export type ModuleClientParams = Record<string, unknown>;
|
||||
|
||||
export type ModuleRouteLayout = "app-sidebar" | "auth" | "app-fullscreen";
|
||||
|
||||
export interface ModuleRouteHandle {
|
||||
layout?: ModuleRouteLayout;
|
||||
protected?: boolean;
|
||||
}
|
||||
|
||||
export type ModuleRouteObject = Omit<RouteObject, "children" | "handle"> & {
|
||||
children?: ModuleRouteObject[];
|
||||
handle?: RouteObject["handle"] & ModuleRouteHandle;
|
||||
};
|
||||
|
||||
export interface IModuleClient extends ModuleMetadata {
|
||||
protected?: boolean; // determina si las rutas deben ser protegidas
|
||||
/**
|
||||
* Default de protección para las rutas del módulo.
|
||||
* Puede sobrescribirse por ruta con handle.protected.
|
||||
*/
|
||||
protected?: boolean;
|
||||
|
||||
/**
|
||||
* Default de layout para las rutas del módulo.
|
||||
* Puede sobrescribirse por ruta con handle.layout.
|
||||
*/
|
||||
layout?: ModuleRouteLayout;
|
||||
|
||||
icon?: ReactNode;
|
||||
routes?: (params: ModuleClientParams) => RouteObject[];
|
||||
layout?: "app" | "auth";
|
||||
|
||||
routes?: (params: ModuleClientParams) => ModuleRouteObject[];
|
||||
}
|
||||
|
||||
@ -30,20 +30,42 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
||||
return [
|
||||
{
|
||||
path: "proformas",
|
||||
handle: {
|
||||
layout: "app-sidebar",
|
||||
protected: true,
|
||||
},
|
||||
element: (
|
||||
<ProformaLayout>
|
||||
<Outlet context={params} />
|
||||
</ProformaLayout>
|
||||
),
|
||||
children: [
|
||||
{ index: true, element: <ProformasListPage /> }, // index
|
||||
{ path: "list", element: <ProformasListPage /> },
|
||||
//{ path: "create", element: <ProformaCreatePage /> },
|
||||
{ path: ":id/edit", element: <ProformaUpdatePage /> },
|
||||
{
|
||||
index: true,
|
||||
element: <ProformasListPage />,
|
||||
},
|
||||
{
|
||||
path: "list",
|
||||
element: <ProformasListPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: "proformas/:id/edit",
|
||||
handle: {
|
||||
layout: "app-fullscreen",
|
||||
protected: true,
|
||||
},
|
||||
element: <ProformaUpdatePage />,
|
||||
},
|
||||
|
||||
{
|
||||
path: "customer-invoices",
|
||||
handle: {
|
||||
layout: "app-sidebar",
|
||||
protected: true,
|
||||
},
|
||||
element: (
|
||||
<IssuedInvoicesLayout>
|
||||
<Outlet context={params} />
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { IModuleClient, ModuleClientParams } from "@erp/core/client";
|
||||
import type { IModuleClient } from "@erp/core/client";
|
||||
|
||||
import { CustomerInvoiceRoutes } from "./customer-invoice-routes";
|
||||
|
||||
@ -9,12 +9,11 @@ export const CustomerInvoicesModuleManifest: IModuleClient = {
|
||||
name: MODULE_NAME,
|
||||
version: MODULE_VERSION,
|
||||
dependencies: ["auth", "Core", "Catalogs", "Customers"],
|
||||
protected: true,
|
||||
layout: "app",
|
||||
|
||||
routes: (params: ModuleClientParams) => {
|
||||
return CustomerInvoiceRoutes(params);
|
||||
},
|
||||
protected: true, // protegido por defecto
|
||||
layout: "app-sidebar", // layout por defecto
|
||||
|
||||
routes: (params) => CustomerInvoiceRoutes(params),
|
||||
};
|
||||
|
||||
export default CustomerInvoicesModuleManifest;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
AmountField,
|
||||
FormFieldLabel,
|
||||
LineDescriptionField,
|
||||
PercentageField,
|
||||
QuantityField,
|
||||
@ -14,6 +15,7 @@ import type {
|
||||
ProformaItemField,
|
||||
ProformaItemsTotals,
|
||||
} from "../../controllers";
|
||||
import type { ProformaTotals } from "../../entities";
|
||||
|
||||
import { LineEditor, type LineEditorColumn } from "./line-editor";
|
||||
|
||||
@ -25,7 +27,8 @@ interface ProformaLineEditorProps {
|
||||
getItemAmounts: (index: number) => ProformaItemAmounts;
|
||||
getItemErrorMessage: (index: number) => string | undefined;
|
||||
|
||||
totals: ProformaItemsTotals;
|
||||
itemsTotals: ProformaItemsTotals;
|
||||
totals: ProformaTotals;
|
||||
|
||||
addItemAtStart: () => void;
|
||||
appendItem: () => void;
|
||||
@ -39,6 +42,7 @@ interface ProformaLineEditorProps {
|
||||
currency?: string;
|
||||
className?: string;
|
||||
}
|
||||
2;
|
||||
|
||||
export const ProformaLineEditor = ({
|
||||
fields,
|
||||
@ -48,6 +52,7 @@ export const ProformaLineEditor = ({
|
||||
getItemAmounts,
|
||||
getItemErrorMessage,
|
||||
|
||||
itemsTotals,
|
||||
totals,
|
||||
|
||||
addItemAtStart,
|
||||
@ -94,7 +99,7 @@ export const ProformaLineEditor = ({
|
||||
{
|
||||
id: "unitAmount",
|
||||
header: t("form_fields.items.unit_amount.label", "Importe unitario"),
|
||||
headClassName: "text-right",
|
||||
headClassName: "w-[200px] text-right",
|
||||
cell: ({ index }) => (
|
||||
<AmountField
|
||||
inputClassName="border-none"
|
||||
@ -140,7 +145,7 @@ export const ProformaLineEditor = ({
|
||||
{
|
||||
id: "total",
|
||||
header: t("form_fields.items.total.label", "Total"),
|
||||
headClassName: "w-[120px] text-right",
|
||||
headClassName: "w-[200px] text-right",
|
||||
className: "text-right font-semibold tabular-nums pt-4",
|
||||
cell: ({ index }) => MoneyHelper.formatCurrency(getItemAmounts(index).total, 2, currency),
|
||||
},
|
||||
@ -174,6 +179,79 @@ export const ProformaLineEditor = ({
|
||||
onRemove={removeItem}
|
||||
removeLabel={t("common.remove", "Eliminar")}
|
||||
renderFooter={() => (
|
||||
<ProformaLineFooterEditor currency={currency} itemsTotals={itemsTotals} totals={totals} />
|
||||
)}
|
||||
title={t("form_fields.items.title", "Líneas de detalle")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ProformaLineFooterEditorProps = Pick<
|
||||
ProformaLineEditorProps,
|
||||
"totals" | "itemsTotals" | "currency"
|
||||
>;
|
||||
|
||||
export const ProformaLineFooterEditor = ({
|
||||
totals,
|
||||
itemsTotals,
|
||||
currency,
|
||||
}: ProformaLineFooterEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex items-end justify-between mt-4 pt-4 border-t border-gray-200">
|
||||
{/* Global discount */}
|
||||
<div className="flex items-center gap-3">
|
||||
<FormFieldLabel>Descuento global (%):</FormFieldLabel>
|
||||
<div className="flex items-center gap-2 w-24">
|
||||
<PercentageField
|
||||
maxFractionDigits={2}
|
||||
minFractionDigits={0}
|
||||
name={"globalDiscountPercentage"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-3">
|
||||
<div className="flex items-baseline gap-4">
|
||||
{t("form_fields.items.total.label", "Total dto. global")}:
|
||||
<span className=" font-medium text-amber-600 tabular-nums">
|
||||
{MoneyHelper.formatCurrency(totals.globalDiscountAmount, 2, currency)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/*totals.descuentoGlobal > 0 && (
|
||||
<span className="text-sm text-amber-600 font-medium">
|
||||
-{MoneyHelper.formatCurrency(descuentoGlobal)}
|
||||
</span>
|
||||
)*/}
|
||||
</div>
|
||||
|
||||
{/* Totals summary */}
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="px-6 py-3">
|
||||
<div className="flex items-baseline gap-4">
|
||||
{t("form_fields.items.total.label", "Total dto. líneas")}:
|
||||
<span className=" font-medium text-amber-600 tabular-nums">
|
||||
{MoneyHelper.formatCurrency(itemsTotals.discountAmount, 2, currency)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border px-6 py-3">
|
||||
<div className="flex items-baseline gap-4">
|
||||
{t("form_fields.items.total.label", "Total")}:
|
||||
<span className=" font-bold tabular-nums">
|
||||
{MoneyHelper.formatCurrency(itemsTotals.total, 2, currency)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
(
|
||||
<div className="flex justify-end">
|
||||
<div className="rounded-md border bg-muted/50 px-6 py-3">
|
||||
<div className="flex items-baseline gap-4">
|
||||
@ -186,8 +264,5 @@ export const ProformaLineEditor = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
title={t("form_fields.items.title", "Líneas de detalle")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
)
|
||||
*/
|
||||
|
||||
@ -18,7 +18,6 @@ import type {
|
||||
UseUpdateProformaTaxControllerResult,
|
||||
UseUpdateProformaTotalsControllerResult,
|
||||
} from "../../controllers";
|
||||
import { EditorSidebar } from "../blocks";
|
||||
import { NewProformaTotalsSummary } from "../blocks/new-proforma-totals-summary";
|
||||
|
||||
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
|
||||
@ -72,35 +71,6 @@ export const ProformaUpdateEditorForm = ({
|
||||
onKeyDown={preventEnterKeySubmitForm}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{/* Contenido principal */}
|
||||
<div className="flex flex-1 overflow-hidden hidden">
|
||||
{/* Área principal */}
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<div className="mx-auto space-y-6 p-4 pb-24 lg:p-6">
|
||||
{/* Formulario de datos básicos */}
|
||||
<ProformaUpdateHeaderEditor className="2xl:col-span-full" disabled={isSubmitting} />
|
||||
|
||||
{/* Tabla de líneas */}
|
||||
<ProformaUpdateItemsEditor
|
||||
disabled={isSubmitting}
|
||||
itemsCtrl={itemsCtrl}
|
||||
taxCtrl={taxCtrl}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Sidebar - desktop */}
|
||||
<aside className="w-96 shrink-0 border-none bg-transparent lg:block">
|
||||
<EditorSidebar
|
||||
className="2xl:col-span-1"
|
||||
disabled={isSubmitting}
|
||||
onChangeCustomerClick={onChangeCustomerClick}
|
||||
onCreateCustomerClick={onCreateCustomerClick}
|
||||
selectedCustomer={selectedCustomer}
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 2xl:grid-cols-4 gap-4 2xl:gap-6">
|
||||
<div className="2xl:col-span-3 space-y-4 gap-4 2xl:gap-6 2xl:space-y-6">
|
||||
<div className="grid 2xl:grid-cols-3 gap-4 2xl:gap-6 space-y-4 2xl:space-y-6">
|
||||
@ -111,6 +81,7 @@ export const ProformaUpdateEditorForm = ({
|
||||
disabled={isSubmitting}
|
||||
itemsCtrl={itemsCtrl}
|
||||
taxCtrl={taxCtrl}
|
||||
totalsCtrl={totalsCtrl}
|
||||
/>
|
||||
</div>
|
||||
<div className="2xl:col-span-1 space-y-4 2xl:space-y-6">
|
||||
|
||||
@ -3,22 +3,28 @@ import { ListIcon } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { UseUpdateProformaTaxControllerResult } from "../../controllers";
|
||||
import type { UseUpdateProformaItemsControllerResult } from "../../controllers/use-update-proforma-items-controller";
|
||||
import type {
|
||||
UseUpdateProformaItemsControllerResult,
|
||||
UseUpdateProformaTaxControllerResult,
|
||||
UseUpdateProformaTotalsControllerResult,
|
||||
} from "../../controllers";
|
||||
import { ProformaLineEditor } from "../blocks";
|
||||
|
||||
interface ProformaUpdateItemsEditorProps extends ComponentProps<"fieldset"> {
|
||||
totalsCtrl: UseUpdateProformaTotalsControllerResult;
|
||||
itemsCtrl: UseUpdateProformaItemsControllerResult;
|
||||
taxCtrl: UseUpdateProformaTaxControllerResult;
|
||||
}
|
||||
|
||||
export const ProformaUpdateItemsEditor = ({
|
||||
totalsCtrl,
|
||||
itemsCtrl,
|
||||
taxCtrl,
|
||||
disabled,
|
||||
...props
|
||||
}: ProformaUpdateItemsEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { totals } = totalsCtrl;
|
||||
|
||||
return (
|
||||
<FormSectionCard
|
||||
@ -35,11 +41,12 @@ export const ProformaUpdateItemsEditor = ({
|
||||
getItemErrorMessage={itemsCtrl.getItemErrorMessage}
|
||||
insertItemAfter={itemsCtrl.insertItemAfter}
|
||||
insertItemBefore={itemsCtrl.insertItemBefore}
|
||||
itemsTotals={itemsCtrl.totals}
|
||||
moveItemDown={itemsCtrl.moveItemDown}
|
||||
moveItemUp={itemsCtrl.moveItemUp}
|
||||
removeItem={itemsCtrl.removeItem}
|
||||
showLineTaxes={taxCtrl.usesPerLineTax}
|
||||
totals={itemsCtrl.totals}
|
||||
totals={totals}
|
||||
/>
|
||||
</FormSectionCard>
|
||||
);
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
SwitchField,
|
||||
} from "@repo/rdx-ui/components";
|
||||
import { PercentageHelper } from "@repo/rdx-utils";
|
||||
import { Separator } from "@repo/shadcn-ui/components";
|
||||
import { ReceiptTextIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
@ -42,7 +43,7 @@ export const ProformaUpdateTaxEditor = ({
|
||||
)}
|
||||
disabled={disabled}
|
||||
icon={<ReceiptTextIcon className="size-5" />}
|
||||
title={t("form_groups.proformas.taxes.title", "Impuestos")}
|
||||
title={t("form_groups.proformas.taxes.title", "Impuestos y retenciones")}
|
||||
>
|
||||
<FormSectionGrid>
|
||||
<SelectField
|
||||
@ -134,6 +135,8 @@ export const ProformaUpdateTaxEditor = ({
|
||||
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
||||
/>
|
||||
|
||||
<Separator className="w-full col-start-1 col-span-full" />
|
||||
|
||||
<SwitchField
|
||||
checked={taxCtrl.usesSingleTax}
|
||||
className="md:col-span-12 md:col-start-1 not-disabled:cursor-pointer"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user