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 {
|
||||||
import type { JSX } from "react";
|
IModuleClient,
|
||||||
import { type RouteObject, useRoutes } from "react-router-dom";
|
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[];
|
modules: IModuleClient[];
|
||||||
params: ModuleClientParams;
|
params: ModuleClientParams;
|
||||||
|
layout: ModuleRouteLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WarpIfProtectedProps {
|
interface NormalizeRouteParams {
|
||||||
component: JSX.Element;
|
route: ModuleRouteObject;
|
||||||
isProtected: boolean;
|
module: IModuleClient;
|
||||||
|
targetLayout: ModuleRouteLayout;
|
||||||
|
inheritedLayout?: ModuleRouteLayout;
|
||||||
|
inheritedProtected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/correctness/noUnusedVariables: <posible uso futuro>
|
const DEFAULT_LAYOUT: ModuleRouteLayout = "app-sidebar";
|
||||||
const WarpIfProtected = ({ component, isProtected }: WarpIfProtectedProps) => {
|
|
||||||
return isProtected ? <>{component}</> : component;
|
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[] = [];
|
const routes: RouteObject[] = [];
|
||||||
|
|
||||||
if (modules) {
|
for (const module of modules) {
|
||||||
for (const module of modules) {
|
if (typeof module.routes !== "function") {
|
||||||
if (typeof module.routes !== "function") {
|
console.warn(`[ModuleRoutes] El módulo "${module.name}" no define una función routes().`);
|
||||||
console.warn(`[ModuleRoutes] El módulo "${module.name}" no define una función 'routes()'`);
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const moduleRoutes = module.routes(params);
|
const moduleRoutes = module.routes(params);
|
||||||
|
|
||||||
if (!Array.isArray(moduleRoutes)) {
|
if (!Array.isArray(moduleRoutes)) {
|
||||||
console.error(
|
console.error(
|
||||||
`[ModuleRoutes] El módulo "${module.name}" debe devolver un RouteObject[], pero devolvió:`,
|
`[ModuleRoutes] El módulo "${module.name}" debe devolver un ModuleRouteObject[], pero devolvió:`,
|
||||||
moduleRoutes
|
moduleRoutes
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allAreRouteObjects = moduleRoutes.every(
|
|
||||||
(r) => typeof r === "object" && r.element !== undefined
|
|
||||||
);
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!allAreRouteObjects) {
|
for (const route of moduleRoutes) {
|
||||||
console.error(
|
const normalizedRoute = normalizeRoute({
|
||||||
`[ModuleRoutes] El módulo "${module.name}" contiene elementos inválidos en su RouteObject[]`,
|
route,
|
||||||
moduleRoutes
|
module,
|
||||||
);
|
targetLayout: layout,
|
||||||
continue;
|
});
|
||||||
|
|
||||||
|
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 { AppSidebar } from "./app-sidebar";
|
||||||
import { AppTopbar } from "./app-topbar";
|
import { AppTopbar } from "./app-topbar";
|
||||||
|
|
||||||
export const AppLayout = () => {
|
export const AppSidebarLayout = () => {
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<SidebarProvider
|
||||||
style={
|
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-fullscreen-layout";
|
||||||
export * from './app-main';
|
export * from "./app-main";
|
||||||
export * from './app-sidebar';
|
export * from "./app-sidebar";
|
||||||
export * from './app-topbar';
|
export * from "./app-sidebar-layout";
|
||||||
|
export * from "./app-topbar";
|
||||||
|
export * from "./auth-layout";
|
||||||
|
|||||||
@ -1,66 +1,103 @@
|
|||||||
// apps/web/src/routes/app-routes.tsx
|
// 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 { Navigate, createBrowserRouter } from "react-router-dom";
|
||||||
import { AppLayout } from "@/layout";
|
|
||||||
|
import { buildModuleRoutes } from "@/components/module-routes";
|
||||||
|
import { AppFullscreenLayout, AppSidebarLayout, AuthLayout } from "@/layout";
|
||||||
import ShadcnShowcasePage from "@/pages/shadcn-ui-page";
|
import ShadcnShowcasePage from "@/pages/shadcn-ui-page";
|
||||||
import TailwindV4ShowcasePage from "@/pages/tailwindcss-page";
|
import TailwindV4ShowcasePage from "@/pages/tailwindcss-page";
|
||||||
|
|
||||||
import { ErrorPage, LoginForm } from "../pages";
|
import { ErrorPage } from "../pages";
|
||||||
import { modules } from "../register-modules";
|
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 = () => {
|
export const getAppRouter = () => {
|
||||||
const params = {
|
const params = {
|
||||||
...import.meta.env,
|
...import.meta.env,
|
||||||
};
|
};
|
||||||
|
|
||||||
const grouped = groupModulesByLayout(modules);
|
const authRoutes = buildModuleRoutes({
|
||||||
|
modules,
|
||||||
|
params,
|
||||||
|
layout: "auth",
|
||||||
|
});
|
||||||
|
|
||||||
return createBrowserRouter(
|
const appSidebarRoutes = buildModuleRoutes({
|
||||||
createRoutesFromElements(
|
modules,
|
||||||
<Route path="/">
|
params,
|
||||||
<Route element={<Navigate replace to="/proformas" />} index />
|
layout: "app-sidebar",
|
||||||
|
});
|
||||||
|
|
||||||
{/* Auth Layout */}
|
const appFullscreenRoutes = buildModuleRoutes({
|
||||||
<Route path="auth">
|
modules,
|
||||||
<Route element={<Navigate replace to="login" />} index />
|
params,
|
||||||
<Route element={<LoginForm />} path="login" />
|
layout: "app-fullscreen",
|
||||||
<Route element={<ModuleRoutes modules={grouped.auth} params={params} />} path="*" />
|
});
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* App Layout */}
|
const appRoutes = createBrowserRouter([
|
||||||
<Route element={<AppLayout />}>
|
{
|
||||||
{/* Test */}
|
path: "/",
|
||||||
<Route element={<ShadcnShowcasePage />} path="shadcnui" />
|
children: [
|
||||||
<Route element={<TailwindV4ShowcasePage />} path="tailwindcss4" />
|
{
|
||||||
|
index: true,
|
||||||
|
element: <Navigate replace to="/proformas" />,
|
||||||
|
},
|
||||||
|
|
||||||
{/* Static / provisional routes */}
|
{
|
||||||
<Route element={<ErrorPage />} path="dashboard" />
|
path: "auth",
|
||||||
<Route element={<ErrorPage />} path="settings" />
|
element: <AuthLayout />,
|
||||||
<Route element={<ErrorPage />} path="catalog" />
|
children: [
|
||||||
<Route element={<ErrorPage />} path="quotes" />
|
{
|
||||||
|
index: true,
|
||||||
|
element: <Navigate replace to="login" />,
|
||||||
|
},
|
||||||
|
...authRoutes,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
{/* Dynamic module routes. Keep this last. */}
|
{
|
||||||
<Route element={<ModuleRoutes modules={grouped.app} params={params} />} path="*" />
|
element: <AppFullscreenLayout />,
|
||||||
</Route>
|
children: appFullscreenRoutes,
|
||||||
</Route>
|
},
|
||||||
)
|
|
||||||
);
|
{
|
||||||
|
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 type { ModuleClientParams } from "@erp/core/client";
|
||||||
import { Outlet, RouteObject } from "react-router-dom";
|
import type { RouteObject } from "react-router-dom";
|
||||||
import { AuthLayout } from "./components";
|
|
||||||
import { LoginPage } from "./pages";
|
import { LoginPage } from "./pages";
|
||||||
|
|
||||||
export const AuthRoutes = (params: ModuleClientParams): RouteObject[] => {
|
export const AuthRoutes = (params: ModuleClientParams): RouteObject[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "login",
|
||||||
element: (
|
handle: {
|
||||||
<AuthLayout>
|
layout: "auth",
|
||||||
<Outlet context={params} />
|
protected: false,
|
||||||
</AuthLayout>
|
},
|
||||||
),
|
element: <LoginPage />,
|
||||||
children: [
|
},
|
||||||
{
|
{
|
||||||
path: "login",
|
path: "register",
|
||||||
element: <LoginPage />,
|
handle: {
|
||||||
},
|
layout: "auth",
|
||||||
{
|
protected: false,
|
||||||
path: "register",
|
},
|
||||||
element: <div>Register</div>,
|
//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 { AuthRoutes } from "./auth-routes";
|
||||||
|
|
||||||
//import enResources from "../common/locales/en.json";
|
|
||||||
//import esResources from "../common/locales/es.json";
|
|
||||||
|
|
||||||
const MODULE_NAME = "auth";
|
const MODULE_NAME = "auth";
|
||||||
const MODULE_VERSION = "1.0.0";
|
const MODULE_VERSION = "1.0.0";
|
||||||
|
|
||||||
@ -13,13 +10,10 @@ export const AuthModuleManifest: IModuleClient = {
|
|||||||
version: MODULE_VERSION,
|
version: MODULE_VERSION,
|
||||||
dependencies: ["core"],
|
dependencies: ["core"],
|
||||||
protected: false,
|
protected: false,
|
||||||
|
|
||||||
layout: "auth",
|
layout: "auth",
|
||||||
|
|
||||||
routes: (params: ModuleClientParams) => {
|
routes: (params) => AuthRoutes(params),
|
||||||
//i18next.addResourceBundle("en", MODULE_NAME, enResources, true, true);
|
|
||||||
//i18next.addResourceBundle("es", MODULE_NAME, esResources, true, true);
|
|
||||||
return AuthRoutes(params);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthModuleManifest;
|
export default AuthModuleManifest;
|
||||||
|
|||||||
@ -3,11 +3,34 @@ import type { RouteObject } from "react-router-dom";
|
|||||||
|
|
||||||
import type { ModuleMetadata } from "../../../common";
|
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 {
|
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;
|
icon?: ReactNode;
|
||||||
routes?: (params: ModuleClientParams) => RouteObject[];
|
|
||||||
layout?: "app" | "auth";
|
routes?: (params: ModuleClientParams) => ModuleRouteObject[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,20 +30,42 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
path: "proformas",
|
path: "proformas",
|
||||||
|
handle: {
|
||||||
|
layout: "app-sidebar",
|
||||||
|
protected: true,
|
||||||
|
},
|
||||||
element: (
|
element: (
|
||||||
<ProformaLayout>
|
<ProformaLayout>
|
||||||
<Outlet context={params} />
|
<Outlet context={params} />
|
||||||
</ProformaLayout>
|
</ProformaLayout>
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <ProformasListPage /> }, // index
|
{
|
||||||
{ path: "list", element: <ProformasListPage /> },
|
index: true,
|
||||||
//{ path: "create", element: <ProformaCreatePage /> },
|
element: <ProformasListPage />,
|
||||||
{ path: ":id/edit", element: <ProformaUpdatePage /> },
|
},
|
||||||
|
{
|
||||||
|
path: "list",
|
||||||
|
element: <ProformasListPage />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "proformas/:id/edit",
|
||||||
|
handle: {
|
||||||
|
layout: "app-fullscreen",
|
||||||
|
protected: true,
|
||||||
|
},
|
||||||
|
element: <ProformaUpdatePage />,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "customer-invoices",
|
path: "customer-invoices",
|
||||||
|
handle: {
|
||||||
|
layout: "app-sidebar",
|
||||||
|
protected: true,
|
||||||
|
},
|
||||||
element: (
|
element: (
|
||||||
<IssuedInvoicesLayout>
|
<IssuedInvoicesLayout>
|
||||||
<Outlet context={params} />
|
<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";
|
import { CustomerInvoiceRoutes } from "./customer-invoice-routes";
|
||||||
|
|
||||||
@ -9,12 +9,11 @@ export const CustomerInvoicesModuleManifest: IModuleClient = {
|
|||||||
name: MODULE_NAME,
|
name: MODULE_NAME,
|
||||||
version: MODULE_VERSION,
|
version: MODULE_VERSION,
|
||||||
dependencies: ["auth", "Core", "Catalogs", "Customers"],
|
dependencies: ["auth", "Core", "Catalogs", "Customers"],
|
||||||
protected: true,
|
|
||||||
layout: "app",
|
|
||||||
|
|
||||||
routes: (params: ModuleClientParams) => {
|
protected: true, // protegido por defecto
|
||||||
return CustomerInvoiceRoutes(params);
|
layout: "app-sidebar", // layout por defecto
|
||||||
},
|
|
||||||
|
routes: (params) => CustomerInvoiceRoutes(params),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CustomerInvoicesModuleManifest;
|
export default CustomerInvoicesModuleManifest;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AmountField,
|
AmountField,
|
||||||
|
FormFieldLabel,
|
||||||
LineDescriptionField,
|
LineDescriptionField,
|
||||||
PercentageField,
|
PercentageField,
|
||||||
QuantityField,
|
QuantityField,
|
||||||
@ -14,6 +15,7 @@ import type {
|
|||||||
ProformaItemField,
|
ProformaItemField,
|
||||||
ProformaItemsTotals,
|
ProformaItemsTotals,
|
||||||
} from "../../controllers";
|
} from "../../controllers";
|
||||||
|
import type { ProformaTotals } from "../../entities";
|
||||||
|
|
||||||
import { LineEditor, type LineEditorColumn } from "./line-editor";
|
import { LineEditor, type LineEditorColumn } from "./line-editor";
|
||||||
|
|
||||||
@ -25,7 +27,8 @@ interface ProformaLineEditorProps {
|
|||||||
getItemAmounts: (index: number) => ProformaItemAmounts;
|
getItemAmounts: (index: number) => ProformaItemAmounts;
|
||||||
getItemErrorMessage: (index: number) => string | undefined;
|
getItemErrorMessage: (index: number) => string | undefined;
|
||||||
|
|
||||||
totals: ProformaItemsTotals;
|
itemsTotals: ProformaItemsTotals;
|
||||||
|
totals: ProformaTotals;
|
||||||
|
|
||||||
addItemAtStart: () => void;
|
addItemAtStart: () => void;
|
||||||
appendItem: () => void;
|
appendItem: () => void;
|
||||||
@ -39,6 +42,7 @@ interface ProformaLineEditorProps {
|
|||||||
currency?: string;
|
currency?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
2;
|
||||||
|
|
||||||
export const ProformaLineEditor = ({
|
export const ProformaLineEditor = ({
|
||||||
fields,
|
fields,
|
||||||
@ -48,6 +52,7 @@ export const ProformaLineEditor = ({
|
|||||||
getItemAmounts,
|
getItemAmounts,
|
||||||
getItemErrorMessage,
|
getItemErrorMessage,
|
||||||
|
|
||||||
|
itemsTotals,
|
||||||
totals,
|
totals,
|
||||||
|
|
||||||
addItemAtStart,
|
addItemAtStart,
|
||||||
@ -94,7 +99,7 @@ export const ProformaLineEditor = ({
|
|||||||
{
|
{
|
||||||
id: "unitAmount",
|
id: "unitAmount",
|
||||||
header: t("form_fields.items.unit_amount.label", "Importe unitario"),
|
header: t("form_fields.items.unit_amount.label", "Importe unitario"),
|
||||||
headClassName: "text-right",
|
headClassName: "w-[200px] text-right",
|
||||||
cell: ({ index }) => (
|
cell: ({ index }) => (
|
||||||
<AmountField
|
<AmountField
|
||||||
inputClassName="border-none"
|
inputClassName="border-none"
|
||||||
@ -140,7 +145,7 @@ export const ProformaLineEditor = ({
|
|||||||
{
|
{
|
||||||
id: "total",
|
id: "total",
|
||||||
header: t("form_fields.items.total.label", "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",
|
className: "text-right font-semibold tabular-nums pt-4",
|
||||||
cell: ({ index }) => MoneyHelper.formatCurrency(getItemAmounts(index).total, 2, currency),
|
cell: ({ index }) => MoneyHelper.formatCurrency(getItemAmounts(index).total, 2, currency),
|
||||||
},
|
},
|
||||||
@ -174,6 +179,79 @@ export const ProformaLineEditor = ({
|
|||||||
onRemove={removeItem}
|
onRemove={removeItem}
|
||||||
removeLabel={t("common.remove", "Eliminar")}
|
removeLabel={t("common.remove", "Eliminar")}
|
||||||
renderFooter={() => (
|
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="flex justify-end">
|
||||||
<div className="rounded-md border bg-muted/50 px-6 py-3">
|
<div className="rounded-md border bg-muted/50 px-6 py-3">
|
||||||
<div className="flex items-baseline gap-4">
|
<div className="flex items-baseline gap-4">
|
||||||
@ -186,8 +264,5 @@ export const ProformaLineEditor = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
title={t("form_fields.items.title", "Líneas de detalle")}
|
*/
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import type {
|
|||||||
UseUpdateProformaTaxControllerResult,
|
UseUpdateProformaTaxControllerResult,
|
||||||
UseUpdateProformaTotalsControllerResult,
|
UseUpdateProformaTotalsControllerResult,
|
||||||
} from "../../controllers";
|
} from "../../controllers";
|
||||||
import { EditorSidebar } from "../blocks";
|
|
||||||
import { NewProformaTotalsSummary } from "../blocks/new-proforma-totals-summary";
|
import { NewProformaTotalsSummary } from "../blocks/new-proforma-totals-summary";
|
||||||
|
|
||||||
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
|
import { ProformaUpdateHeaderEditor } from "./proforma-update-header-editor";
|
||||||
@ -72,35 +71,6 @@ export const ProformaUpdateEditorForm = ({
|
|||||||
onKeyDown={preventEnterKeySubmitForm}
|
onKeyDown={preventEnterKeySubmitForm}
|
||||||
onSubmit={onSubmit}
|
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="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="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">
|
<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}
|
disabled={isSubmitting}
|
||||||
itemsCtrl={itemsCtrl}
|
itemsCtrl={itemsCtrl}
|
||||||
taxCtrl={taxCtrl}
|
taxCtrl={taxCtrl}
|
||||||
|
totalsCtrl={totalsCtrl}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="2xl:col-span-1 space-y-4 2xl:space-y-6">
|
<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 type { ComponentProps } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
import type { UseUpdateProformaTaxControllerResult } from "../../controllers";
|
import type {
|
||||||
import type { UseUpdateProformaItemsControllerResult } from "../../controllers/use-update-proforma-items-controller";
|
UseUpdateProformaItemsControllerResult,
|
||||||
|
UseUpdateProformaTaxControllerResult,
|
||||||
|
UseUpdateProformaTotalsControllerResult,
|
||||||
|
} from "../../controllers";
|
||||||
import { ProformaLineEditor } from "../blocks";
|
import { ProformaLineEditor } from "../blocks";
|
||||||
|
|
||||||
interface ProformaUpdateItemsEditorProps extends ComponentProps<"fieldset"> {
|
interface ProformaUpdateItemsEditorProps extends ComponentProps<"fieldset"> {
|
||||||
|
totalsCtrl: UseUpdateProformaTotalsControllerResult;
|
||||||
itemsCtrl: UseUpdateProformaItemsControllerResult;
|
itemsCtrl: UseUpdateProformaItemsControllerResult;
|
||||||
taxCtrl: UseUpdateProformaTaxControllerResult;
|
taxCtrl: UseUpdateProformaTaxControllerResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProformaUpdateItemsEditor = ({
|
export const ProformaUpdateItemsEditor = ({
|
||||||
|
totalsCtrl,
|
||||||
itemsCtrl,
|
itemsCtrl,
|
||||||
taxCtrl,
|
taxCtrl,
|
||||||
disabled,
|
disabled,
|
||||||
...props
|
...props
|
||||||
}: ProformaUpdateItemsEditorProps) => {
|
}: ProformaUpdateItemsEditorProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { totals } = totalsCtrl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSectionCard
|
<FormSectionCard
|
||||||
@ -35,11 +41,12 @@ export const ProformaUpdateItemsEditor = ({
|
|||||||
getItemErrorMessage={itemsCtrl.getItemErrorMessage}
|
getItemErrorMessage={itemsCtrl.getItemErrorMessage}
|
||||||
insertItemAfter={itemsCtrl.insertItemAfter}
|
insertItemAfter={itemsCtrl.insertItemAfter}
|
||||||
insertItemBefore={itemsCtrl.insertItemBefore}
|
insertItemBefore={itemsCtrl.insertItemBefore}
|
||||||
|
itemsTotals={itemsCtrl.totals}
|
||||||
moveItemDown={itemsCtrl.moveItemDown}
|
moveItemDown={itemsCtrl.moveItemDown}
|
||||||
moveItemUp={itemsCtrl.moveItemUp}
|
moveItemUp={itemsCtrl.moveItemUp}
|
||||||
removeItem={itemsCtrl.removeItem}
|
removeItem={itemsCtrl.removeItem}
|
||||||
showLineTaxes={taxCtrl.usesPerLineTax}
|
showLineTaxes={taxCtrl.usesPerLineTax}
|
||||||
totals={itemsCtrl.totals}
|
totals={totals}
|
||||||
/>
|
/>
|
||||||
</FormSectionCard>
|
</FormSectionCard>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
SwitchField,
|
SwitchField,
|
||||||
} from "@repo/rdx-ui/components";
|
} from "@repo/rdx-ui/components";
|
||||||
import { PercentageHelper } from "@repo/rdx-utils";
|
import { PercentageHelper } from "@repo/rdx-utils";
|
||||||
|
import { Separator } from "@repo/shadcn-ui/components";
|
||||||
import { ReceiptTextIcon } from "lucide-react";
|
import { ReceiptTextIcon } from "lucide-react";
|
||||||
|
|
||||||
import { useTranslation } from "../../../../i18n";
|
import { useTranslation } from "../../../../i18n";
|
||||||
@ -42,7 +43,7 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
)}
|
)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
icon={<ReceiptTextIcon className="size-5" />}
|
icon={<ReceiptTextIcon className="size-5" />}
|
||||||
title={t("form_groups.proformas.taxes.title", "Impuestos")}
|
title={t("form_groups.proformas.taxes.title", "Impuestos y retenciones")}
|
||||||
>
|
>
|
||||||
<FormSectionGrid>
|
<FormSectionGrid>
|
||||||
<SelectField
|
<SelectField
|
||||||
@ -134,6 +135,8 @@ export const ProformaUpdateTaxEditor = ({
|
|||||||
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
serialize={(value) => (typeof value === "number" ? String(value) : "")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Separator className="w-full col-start-1 col-span-full" />
|
||||||
|
|
||||||
<SwitchField
|
<SwitchField
|
||||||
checked={taxCtrl.usesSingleTax}
|
checked={taxCtrl.usesSingleTax}
|
||||||
className="md:col-span-12 md:col-start-1 not-disabled:cursor-pointer"
|
className="md:col-span-12 md:col-start-1 not-disabled:cursor-pointer"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user