Pantalla update fullscreen

This commit is contained in:
David Arranz 2026-06-02 18:40:23 +02:00
parent 823e5cebe1
commit 66623cbe78
17 changed files with 460 additions and 174 deletions

View File

@ -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;
};

View 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>
);
};

View File

@ -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={

View 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>
);
};

View File

@ -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";

View File

@ -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;
};

View 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}</>;
};

View 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}</>;
};

View File

@ -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 />,
},
];
};

View File

@ -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;

View File

@ -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[];
}

View File

@ -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} />

View File

@ -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;

View File

@ -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")}
/>
);
};
)
*/

View File

@ -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">

View File

@ -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>
);

View File

@ -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"