diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 5ae2764c..2455523a 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -19,7 +19,7 @@ export function createApp(config: ConfigType): Application { // En desarrollo reflejamos el Origin entrante (permite credenciales) origin: true, credentials: true, - methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], exposedHeaders: [ "Content-Disposition", "Content-Type", @@ -34,7 +34,7 @@ export function createApp(config: ConfigType): Application { const prodCors: CorsOptions = { origin: config.server.frontendUrl, credentials: true, - methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], exposedHeaders: [ "Content-Disposition", "Content-Type", diff --git a/apps/web/package.json b/apps/web/package.json index f49f172f..071dc17a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -50,6 +50,7 @@ "react-error-boundary": "^6.1.1", "react-hook-form": "^7.72.1", "react-i18next": "^17.0.2", + "react-router": "^7.14.0", "react-router-dom": "^7.14.0", "react-secure-storage": "^1.3.2", "sequelize": "^6.37.8", diff --git a/apps/web/src/layout/app-company-switcher.tsx b/apps/web/src/layout/app-company-switcher.tsx new file mode 100644 index 00000000..925d3484 --- /dev/null +++ b/apps/web/src/layout/app-company-switcher.tsx @@ -0,0 +1,108 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { CheckIcon, ChevronDownIcon } from "lucide-react"; +import * as React from "react"; + +interface Company { + id: string; + name: string; + taxName: string; + initials: string; +} + +const companies: Company[] = [ + { id: "company-1", name: "Mi Empresa", taxName: "Mi Empresa S.L.", initials: "ME" }, + { id: "company-2", name: "Comercial Norte", taxName: "Comercial Norte S.L.", initials: "CN" }, + { + id: "company-3", + name: "Servicios Globales", + taxName: "Servicios Globales S.L.", + initials: "SG", + }, +]; + +interface AppCompanySwitcherProps { + className?: string; +} + +export const AppCompanySwitcher = ({ className }: AppCompanySwitcherProps) => { + const [selectedCompany, setSelectedCompany] = React.useState(companies[0]); + + return ( +
+ + + + {selectedCompany.initials} + + + + + {selectedCompany.name} + + + {selectedCompany.taxName} + + + + +
+ ); +}; diff --git a/apps/web/src/layout/app-layout.tsx b/apps/web/src/layout/app-layout.tsx new file mode 100644 index 00000000..db18620e --- /dev/null +++ b/apps/web/src/layout/app-layout.tsx @@ -0,0 +1,28 @@ +import { SidebarInset, SidebarProvider } from "@repo/shadcn-ui/components"; +import type * as React from "react"; +import { Outlet } from "react-router-dom"; + +import { AppMain } from "./app-main"; +import { AppSidebar } from "./app-sidebar"; +import { AppTopbar } from "./app-topbar"; + +export const AppLayout = () => { + return ( + + + + + + + + + + ); +}; diff --git a/apps/web/src/layout/app-main.tsx b/apps/web/src/layout/app-main.tsx new file mode 100644 index 00000000..1a69517b --- /dev/null +++ b/apps/web/src/layout/app-main.tsx @@ -0,0 +1,11 @@ +import { cn } from "@repo/shadcn-ui/lib/utils"; +import type * as React from "react"; + +interface AppMainProps { + children: React.ReactNode; + className?: string; +} + +export const AppMain = ({ children, className }: AppMainProps) => { + return
{children}
; +}; diff --git a/apps/web/src/layout/app-sidebar-nav.tsx b/apps/web/src/layout/app-sidebar-nav.tsx new file mode 100644 index 00000000..6f700ac2 --- /dev/null +++ b/apps/web/src/layout/app-sidebar-nav.tsx @@ -0,0 +1,162 @@ +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { ChevronDownIcon } from "lucide-react"; +import * as React from "react"; +import { Link } from "react-router-dom"; + +import type { AppSidebarNavSection, AppSidebarSectionAccent } from "./app-sidebar.config"; + +interface AppSidebarNavProps { + sections: AppSidebarNavSection[]; +} + +interface SidebarSectionAccentStyles { + sectionBorder: string; + sectionLabel: string; + activeItem: string; + activeIcon: string; + inactiveIcon: string; +} + +export const AppSidebarNav = ({ sections }: AppSidebarNavProps) => { + const [openSections, setOpenSections] = React.useState>(() => + Object.fromEntries(sections.map((s) => [s.title, true])) + ); + + const sectionAccentStyles: Record = { + blue: { + sectionBorder: "border-l-blue-500", + sectionLabel: "text-blue-700", + activeItem: "border-l-blue-500 bg-blue-50 text-blue-700 hover:bg-blue-50 hover:text-blue-700", + activeIcon: "text-blue-600", + inactiveIcon: "text-muted-foreground", + }, + green: { + sectionBorder: "border-l-emerald-500", + sectionLabel: "text-emerald-700", + activeItem: + "border-l-emerald-500 bg-emerald-50 text-emerald-700 hover:bg-emerald-50 hover:text-emerald-700", + activeIcon: "text-emerald-600", + inactiveIcon: "text-muted-foreground", + }, + amber: { + sectionBorder: "border-l-amber-500", + sectionLabel: "text-amber-700", + activeItem: + "border-l-amber-500 bg-amber-50 text-amber-700 hover:bg-amber-50 hover:text-amber-700", + activeIcon: "text-amber-600", + inactiveIcon: "text-muted-foreground", + }, + violet: { + sectionBorder: "border-l-violet-500", + sectionLabel: "text-violet-700", + activeItem: + "border-l-violet-500 bg-violet-50 text-violet-700 hover:bg-violet-50 hover:text-violet-700", + activeIcon: "text-violet-600", + inactiveIcon: "text-muted-foreground", + }, + }; + + return ( + + ); +}; diff --git a/apps/web/src/layout/app-sidebar-settings-menu.tsx b/apps/web/src/layout/app-sidebar-settings-menu.tsx new file mode 100644 index 00000000..27d69aba --- /dev/null +++ b/apps/web/src/layout/app-sidebar-settings-menu.tsx @@ -0,0 +1,104 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { + Building2Icon, + ChevronRightIcon, + CoinsIcon, + PercentIcon, + SettingsIcon, + SlidersHorizontalIcon, + UserCogIcon, +} from "lucide-react"; +import { Link } from "react-router-dom"; + +interface AppSidebarSettingsItem { + title: string; + href: string; + icon: typeof UserCogIcon; +} + +const settingsItems: AppSidebarSettingsItem[] = [ + { title: "Usuarios y roles", href: "/settings/users", icon: UserCogIcon }, + { title: "Empresas", href: "/settings/companies", icon: Building2Icon }, + { title: "Impuestos", href: "/settings/taxes", icon: PercentIcon }, + { title: "Monedas", href: "/settings/currencies", icon: CoinsIcon }, + { title: "Parámetros", href: "/settings", icon: SlidersHorizontalIcon }, +]; + +interface AppSidebarSettingsMenuProps { + className?: string; +} + +export const AppSidebarSettingsMenu = ({ className }: AppSidebarSettingsMenuProps) => { + return ( + + + + + + + + + Configuración + + + + {settingsItems.map((item) => { + const Icon = item.icon; + + return ( + + + + + + + ); +}; diff --git a/apps/web/src/layout/app-sidebar.config.ts b/apps/web/src/layout/app-sidebar.config.ts new file mode 100644 index 00000000..ccf84e7a --- /dev/null +++ b/apps/web/src/layout/app-sidebar.config.ts @@ -0,0 +1,97 @@ +import type { LucideIcon } from "lucide-react"; +import { + BanknoteIcon, + BarChart3Icon, + BoxesIcon, + Building2Icon, + ClipboardListIcon, + CoinsIcon, + CreditCardIcon, + FileTextIcon, + LandmarkIcon, + PackageIcon, + PercentIcon, + ReceiptIcon, + RefreshCcwIcon, + ShoppingCartIcon, + SlidersHorizontalIcon, + TruckIcon, + UserCogIcon, + UsersIcon, + WarehouseIcon, +} from "lucide-react"; + +export interface AppSidebarNavItem { + title: string; + href: string; + icon: LucideIcon; +} + +export type AppSidebarSectionAccent = "blue" | "green" | "amber" | "violet"; + +export interface AppSidebarNavSection { + title: string; + accent: AppSidebarSectionAccent; + items: AppSidebarNavItem[]; +} + +export const appSidebarNavSections: AppSidebarNavSection[] = [ + { + title: "Ventas", + accent: "blue", + items: [ + { title: "Proformas", href: "/proformas", icon: FileTextIcon }, + { title: "Pedidos de venta", href: "/sales-orders", icon: ShoppingCartIcon }, + { title: "Clientes", href: "/customers", icon: UsersIcon }, + { title: "Facturación", href: "/customer-invoices", icon: ReceiptIcon }, + { title: "Cobros", href: "/collections", icon: CreditCardIcon }, + { title: "Devoluciones", href: "/sales-returns", icon: RefreshCcwIcon }, + ], + }, + { + title: "Compras", + accent: "green", + items: [ + { title: "Pedidos de compra", href: "/purchase-orders", icon: ClipboardListIcon }, + { title: "Proveedores", href: "/suppliers", icon: TruckIcon }, + { title: "Facturas de compra", href: "/supplier-invoices", icon: ReceiptIcon }, + { title: "Pagos", href: "/payments", icon: BanknoteIcon }, + { title: "Devoluciones", href: "/purchase-returns", icon: RefreshCcwIcon }, + ], + }, + { + title: "Inventario", + accent: "amber", + items: [ + { title: "Productos", href: "/products", icon: PackageIcon }, + { title: "Almacenes", href: "/warehouses", icon: WarehouseIcon }, + { title: "Movimientos", href: "/stock-movements", icon: BoxesIcon }, + { title: "Ajustes de stock", href: "/stock-adjustments", icon: SlidersHorizontalIcon }, + { title: "Categorías", href: "/product-categories", icon: ClipboardListIcon }, + ], + }, + { + title: "Contabilidad", + accent: "violet", + items: [ + { title: "Plan contable", href: "/accounting/chart", icon: LandmarkIcon }, + { title: "Asientos contables", href: "/accounting/entries", icon: FileTextIcon }, + { title: "Diario", href: "/accounting/journal", icon: ClipboardListIcon }, + { title: "Informes financieros", href: "/accounting/reports", icon: BarChart3Icon }, + ], + }, +]; + +export interface AppSidebarSettingsItem { + title: string; + href: string; + icon: LucideIcon; +} + +export const appSidebarSettingsItems: AppSidebarSettingsItem[] = [ + { title: "Usuarios y roles", href: "/settings/users", icon: UserCogIcon }, + { title: "Empresas", href: "/settings/companies", icon: Building2Icon }, + { title: "Impuestos", href: "/settings/taxes", icon: PercentIcon }, + { title: "Monedas", href: "/settings/currencies", icon: CoinsIcon }, + { title: "Parámetros", href: "/settings", icon: SlidersHorizontalIcon }, +]; diff --git a/apps/web/src/layout/app-sidebar.tsx b/apps/web/src/layout/app-sidebar.tsx new file mode 100644 index 00000000..29164d04 --- /dev/null +++ b/apps/web/src/layout/app-sidebar.tsx @@ -0,0 +1,40 @@ +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarRail, +} from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; + +import { AppCompanySwitcher } from "./app-company-switcher"; +import { appSidebarNavSections } from "./app-sidebar.config"; +import { AppSidebarNav } from "./app-sidebar-nav"; +import { AppSidebarSettingsMenu } from "./app-sidebar-settings-menu"; + +interface AppSidebarProps { + className?: string; +} + +export const AppSidebar = ({ className }: AppSidebarProps) => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/web/src/layout/app-topbar.tsx b/apps/web/src/layout/app-topbar.tsx new file mode 100644 index 00000000..0c15159b --- /dev/null +++ b/apps/web/src/layout/app-topbar.tsx @@ -0,0 +1,109 @@ +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, + Input, + Separator, + SidebarTrigger, +} from "@repo/shadcn-ui/components"; +import { BellIcon, ChevronDownIcon, MessageSquareIcon, SearchIcon } from "lucide-react"; + +export const AppTopbar = () => { + return ( +
+
+ + +
+
+
+ + + +
+ + + + + + + + + + + + AM + + + + Ana Martínez + + + Administrador + + + + + } + /> + + + + Mi cuenta + + Perfil + Preferencias + Cerrar sesión + + + +
+
+ ); +}; diff --git a/apps/web/src/layout/index.ts b/apps/web/src/layout/index.ts new file mode 100644 index 00000000..87af5708 --- /dev/null +++ b/apps/web/src/layout/index.ts @@ -0,0 +1,4 @@ +export * from './app-layout'; +export * from './app-main'; +export * from './app-sidebar'; +export * from './app-topbar'; diff --git a/apps/web/src/routes/app-routes.tsx b/apps/web/src/routes/app-routes.tsx index 27571ec6..7f87c707 100644 --- a/apps/web/src/routes/app-routes.tsx +++ b/apps/web/src/routes/app-routes.tsx @@ -1,13 +1,14 @@ +// apps/web/src/routes/app-routes.tsx import type { IModuleClient } from "@erp/core/client"; -import { AppLayout } from "@repo/rdx-ui/components"; import { Navigate, Route, createBrowserRouter, createRoutesFromElements } from "react-router-dom"; import { ModuleRoutes } from "@/components/module-routes"; +import { AppLayout } from "@/layout"; import ShadcnShowcasePage from "@/pages/shadcn-ui-page"; import TailwindV4ShowcasePage from "@/pages/tailwindcss-page"; import { ErrorPage, LoginForm } from "../pages"; -import { modules } from "../register-modules"; // Aquí ca +import { modules } from "../register-modules"; function groupModulesByLayout(modules: IModuleClient[]) { const groups: Record = { @@ -15,14 +16,11 @@ function groupModulesByLayout(modules: IModuleClient[]) { app: [], }; - if (modules) { - for (const module of modules) { - if (typeof module.layout !== "string") continue; + for (const module of modules) { + const layout = typeof module.layout === "string" ? module.layout : "app"; - const layout = module.layout || "app"; - groups[layout] = groups[layout] || []; - groups[layout].push(module); - } + groups[layout] = groups[layout] ?? []; + groups[layout].push(module); } return groups; @@ -38,27 +36,29 @@ export const getAppRouter = () => { return createBrowserRouter( createRoutesFromElements( + } index /> + {/* Auth Layout */} - - } index /> + + } index /> } path="login" /> } path="*" /> {/* App Layout */} }> - {/* Dynamic Module Routes */} - } path="*" /> - {/* Test */} - } path="/shadcnui" /> - } path="/tailwindcss4" /> + } path="shadcnui" /> + } path="tailwindcss4" /> - {/* Main Layout */} - } path="/dashboard" /> - } path="/settings" /> - } path="/catalog" /> - } path="/quotes" /> + {/* Static / provisional routes */} + } path="dashboard" /> + } path="settings" /> + } path="catalog" /> + } path="quotes" /> + + {/* Dynamic module routes. Keep this last. */} + } path="*" /> ) diff --git a/modules/core/src/web/components/form/form-actions/index.ts b/modules/core/src/web/components/form/form-actions/index.ts new file mode 100644 index 00000000..6000c998 --- /dev/null +++ b/modules/core/src/web/components/form/form-actions/index.ts @@ -0,0 +1,2 @@ +export * from './page-form-header'; +export * from './page-keyboard-shortcuts-button'; diff --git a/modules/core/src/web/components/form/form-actions/page-form-header.tsx b/modules/core/src/web/components/form/form-actions/page-form-header.tsx new file mode 100644 index 00000000..8d3990e8 --- /dev/null +++ b/modules/core/src/web/components/form/form-actions/page-form-header.tsx @@ -0,0 +1,78 @@ +// packages/rdx-ui/src/components/form-actions/erp-form-header.tsx +import { Badge, Button } from "@repo/shadcn-ui/components"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { ArrowLeftIcon } from "lucide-react"; +import type { ReactNode } from "react"; + +export interface PageFormHeaderProps { + title: string; + + backLabel: string; + onBack?: () => void; + + statusLabel?: string; + showStatus?: boolean; + + actions?: ReactNode; + children?: ReactNode; + + className?: string; + contentClassName?: string; +} + +export const PageFormHeader = ({ + title, + backLabel, + onBack, + statusLabel, + showStatus = false, + actions, + children, + className, + contentClassName, +}: PageFormHeaderProps) => { + return ( +
+
+
+ + +
+

+ {title} +

+ + {statusLabel ? ( + + {statusLabel} + + ) : null} +
+
+ + {actions ?
{actions}
: null} +
+ + {children} +
+ ); +}; diff --git a/modules/core/src/web/components/form/form-actions/page-keyboard-shortcuts-button.tsx b/modules/core/src/web/components/form/form-actions/page-keyboard-shortcuts-button.tsx new file mode 100644 index 00000000..33f9c9d2 --- /dev/null +++ b/modules/core/src/web/components/form/form-actions/page-keyboard-shortcuts-button.tsx @@ -0,0 +1,50 @@ +import { Button, Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components"; +import { KeyboardIcon } from "lucide-react"; + +export interface PageKeyboardShortcut { + keys: string; + label: string; +} + +export interface PageKeyboardShortcutsButtonProps { + label: string; + shortcuts: PageKeyboardShortcut[]; + + className?: string; +} + +export const PageKeyboardShortcutsButton = ({ + label, + shortcuts, + className, +}: PageKeyboardShortcutsButtonProps) => { + if (shortcuts.length === 0) return null; + + return ( + + + + ); +}; diff --git a/modules/core/src/web/components/form/index.ts b/modules/core/src/web/components/form/index.ts index e7c01d71..786807c8 100644 --- a/modules/core/src/web/components/form/index.ts +++ b/modules/core/src/web/components/form/index.ts @@ -1,2 +1,3 @@ +export * from "./form-actions"; export * from "./form-debug.tsx"; export * from "./simple-search-input.tsx"; diff --git a/modules/core/src/web/components/page-header.tsx b/modules/core/src/web/components/page-header.tsx index d03ce0ae..50779257 100644 --- a/modules/core/src/web/components/page-header.tsx +++ b/modules/core/src/web/components/page-header.tsx @@ -51,14 +51,16 @@ export const PageHeader = ({
-

+

{title}

{statusSlot}
- {description &&

{description}

} + {description && ( +

{description}

+ )}
diff --git a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/cancel-form-button.tsx b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/cancel-form-button.tsx deleted file mode 100644 index 181b3f97..00000000 --- a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/cancel-form-button.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Button } from "@repo/shadcn-ui/components"; -import { cn } from "@repo/shadcn-ui/lib/utils"; -import { XIcon } from "lucide-react"; -import type * as React from "react"; -import { useCallback, useState } from "react"; -import { useNavigate } from "react-router-dom"; - -import { useTranslation } from "../../../i18n.ts"; -import { useUnsavedChangesContext } from "../use-unsaved-changes-notifier"; - -export type CancelFormButtonProps = { - to?: string; - onCancel?: () => void | Promise; - label?: string; - variant?: React.ComponentProps["variant"]; - size?: React.ComponentProps["size"]; - className?: string; - disabled?: boolean; - "data-testid"?: string; -}; - -export const CancelFormButton = ({ - to, - onCancel, - label, - variant = "outline", - size = "default", - className, - disabled = false, - "data-testid": dataTestId = "cancel-button", -}: CancelFormButtonProps) => { - const navigate = useNavigate(); - const { t } = useTranslation(); - const { requestConfirm } = useUnsavedChangesContext(); - - const [isRunning, setIsRunning] = useState(false); - - const defaultLabel = t("common.cancel"); - const computedDisabled = disabled || isRunning; - - const handleClick = useCallback(async () => { - if (computedDisabled) return; - - const ok = requestConfirm ? await requestConfirm() : true; - if (!ok) return; - - try { - setIsRunning(true); - - if (onCancel) { - await onCancel(); - return; - } - - if (to) { - navigate(to); - } - } finally { - setIsRunning(false); - } - }, [computedDisabled, requestConfirm, onCancel, to, navigate]); - - return ( - - ); -}; diff --git a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/form-commit-button-group.tsx b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/form-commit-button-group.tsx deleted file mode 100644 index abb06eaf..00000000 --- a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/form-commit-button-group.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@repo/shadcn-ui/components"; -import { cn } from "@repo/shadcn-ui/lib/utils"; -import { - ArrowLeftIcon, - CopyIcon, - EyeIcon, - MoreHorizontalIcon, - RotateCcwIcon, - Trash2Icon, -} from "lucide-react"; -import { useFormContext } from "react-hook-form"; -import { useTranslation } from "react-i18next"; - -import { CancelFormButton, type CancelFormButtonProps } from "./cancel-form-button"; -import { type SubmitButtonProps, SubmitFormButton } from "./submit-form-button"; - -type Align = "start" | "center" | "end" | "between"; - -type GroupSubmitButtonProps = Omit; - -export type FormCommitButtonGroupProps = { - className?: string; - align?: Align; // default "end" - gap?: string; // default "gap-2" - reverseOrderOnMobile?: boolean; // default true (Cancel debajo en móvil) - - isLoading?: boolean; - disabled?: boolean; - preventDoubleSubmit?: boolean; // Evita múltiples submits mientras loading - - cancel?: CancelFormButtonProps & { show?: boolean }; - submit?: GroupSubmitButtonProps; // props directas a SubmitButton - - onReset?: () => void; - onDelete?: () => void; - onPreview?: () => void; - onDuplicate?: () => void; - onBack?: () => void; -}; - -const alignToJustify: Record = { - start: "justify-start", - center: "justify-center", - end: "justify-end", - between: "justify-between", -}; - -export const FormCommitButtonGroup = ({ - className, - align = "end", - gap = "gap-2", - reverseOrderOnMobile = true, - - isLoading, - disabled = false, - preventDoubleSubmit = true, - - cancel, - submit, - - onReset, - onDelete, - onPreview, - onDuplicate, - onBack, -}: FormCommitButtonGroupProps) => { - const { t } = useTranslation(); - - const ctx = useFormContext(); - const rhfIsSubmitting = ctx.formState.isSubmitting; - const busy = isLoading ?? rhfIsSubmitting; - - const showCancel = cancel?.show ?? true; - const computedDisabled = !!(disabled || (preventDoubleSubmit && busy)); - const hasSecondaryActions = !!(onReset || onPreview || onDuplicate || onBack || onDelete); - - return ( -
- {showCancel && ( - - )} - - {submit && ( - - )} - - {hasSecondaryActions && ( - - - - {t("common.moreActions")} - - } - /> - - - - {onReset && ( - - - {t("common.resetChanges")} - - )} - - {onPreview && ( - - - {t("common.preview")} - - )} - - {onDuplicate && ( - - - {t("common.duplicate")} - - )} - - {onBack && ( - - - {t("common.back")} - - )} - - {onDelete && ( - <> - - - - {t("common.delete")} - - - )} - - - - )} -
- ); -}; diff --git a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/index.ts b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/index.ts index d396a4cb..324b244f 100644 --- a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/index.ts +++ b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/index.ts @@ -1,4 +1,2 @@ -export * from "./cancel-form-button"; -export * from "./form-commit-button-group"; -export * from "./submit-form-button"; +export * from "./unsaved-changes-cancel-action-button"; export * from "./unsaved-changes-dialog"; diff --git a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/submit-form-button.tsx b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/submit-form-button.tsx deleted file mode 100644 index 3f131d46..00000000 --- a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/submit-form-button.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Button } from "@repo/shadcn-ui/components"; -import { cn } from "@repo/shadcn-ui/lib/utils"; -import { LoaderCircleIcon, SaveIcon } from "lucide-react"; -import type * as React from "react"; -import { useFormContext } from "react-hook-form"; - -import { useTranslation } from "../../../i18n.ts"; - -export type SubmitButtonProps = { - formId?: string; - isLoading?: boolean; - label?: string; - labelIsLoading?: string; - - variant?: React.ComponentProps["variant"]; - size?: React.ComponentProps["size"]; - className?: string; - - preventDoubleSubmit?: boolean; - hasChanges?: boolean; - - onClick?: React.MouseEventHandler; - disabled?: boolean; - children?: React.ReactNode; - "data-testid"?: string; -}; - -export const SubmitFormButton = ({ - formId, - isLoading, - label, - labelIsLoading, - variant = "default", - size = "default", - className, - preventDoubleSubmit = true, - hasChanges = false, - onClick, - disabled = false, - children, - "data-testid": dataTestId = "submit-button", -}: SubmitButtonProps) => { - const { t } = useTranslation(); - const { formState } = useFormContext(); - - const busy = isLoading ?? formState.isSubmitting; - const computedDisabled = disabled || (preventDoubleSubmit && busy); - - const handleClick: React.MouseEventHandler = (event) => { - if (preventDoubleSubmit && busy) { - event.preventDefault(); - event.stopPropagation(); - return; - } - - onClick?.(event); - }; - - return ( - - ); -}; diff --git a/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/unsaved-changes-cancel-action-button.tsx b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/unsaved-changes-cancel-action-button.tsx new file mode 100644 index 00000000..4426d9db --- /dev/null +++ b/modules/core/src/web/hooks/use-unsaved-changes-notifier/components/unsaved-changes-cancel-action-button.tsx @@ -0,0 +1,37 @@ +import { CancelActionButton, type CancelActionButtonProps } from "@repo/rdx-ui/components"; +import { useState } from "react"; + +import { useUnsavedChangesContext } from "../use-unsaved-changes-notifier"; + +export type UnsavedChangesCancelActionButtonProps = Omit; + +export const UnsavedChangesCancelActionButton = ({ + onCancel, + disabled, + ...props +}: UnsavedChangesCancelActionButtonProps) => { + const { requestConfirm } = useUnsavedChangesContext(); + const [isRunning, setIsRunning] = useState(false); + + const handleCancel = async () => { + setIsRunning(true); + + try { + const ok = requestConfirm ? await requestConfirm() : true; + if (!ok) return; + + await onCancel(); + } finally { + setIsRunning(false); + } + }; + + return ( + + ); +}; diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts index b583071d..9eeeff21 100644 --- a/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts @@ -15,6 +15,7 @@ import type { } from "../snapshot-builders"; import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full"; import { + ChangeStatusProformaUseCase, CreateProformaUseCase, GetProformaByIdUseCase, IssueProformaUseCase, @@ -112,11 +113,18 @@ export function buildUpdateProformaUseCase(deps: { /* export function buildDeleteProformaUseCase(deps: { finder: IProformaFinder }) { return new DeleteProformaUseCase(deps.finder); -} +}*/ export function buildChangeStatusProformaUseCase(deps: { finder: IProformaFinder; + updater: IProformaUpdater; + fullSnapshotBuilder: IProformaFullSnapshotBuilder; transactionManager: ITransactionManager; }) { - return new ChangeStatusProformaUseCase(deps.finder, deps.transactionManager); -}*/ + return new ChangeStatusProformaUseCase({ + finder: deps.finder, + updater: deps.updater, + fullSnapshotBuilder: deps.fullSnapshotBuilder, + transactionManager: deps.transactionManager, + }); +} diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts index f3229c39..9a66c5ce 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts @@ -3,8 +3,8 @@ import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { ChangeStatusProformaByIdRequestDTO } from "../../../../common"; -import { ProformaCustomerInvoiceDomainService } from "../../../domain"; -import type { CustomerInvoiceApplicationService } from "../../services"; +import type { IProformaFinder, IProformaUpdater } from "../services"; +import type { IProformaFullSnapshotBuilder } from "../snapshot-builders"; type ChangeStatusProformaUseCaseInput = { companyId: UniqueID; @@ -12,14 +12,24 @@ type ChangeStatusProformaUseCaseInput = { dto: ChangeStatusProformaByIdRequestDTO; }; -export class ChangeStatusProformaUseCase { - private readonly proformaDomainService: ProformaCustomerInvoiceDomainService; +type ChangeStatusProformaUseCaseDeps = { + finder: IProformaFinder; + updater: IProformaUpdater; + fullSnapshotBuilder: IProformaFullSnapshotBuilder; + transactionManager: ITransactionManager; +}; - constructor( - private readonly service: CustomerInvoiceApplicationService, - private readonly transactionManager: ITransactionManager - ) { - this.proformaDomainService = new ProformaCustomerInvoiceDomainService(); +export class ChangeStatusProformaUseCase { + private readonly finder: IProformaFinder; + private readonly updater: IProformaUpdater; + private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder; + private readonly transactionManager: ITransactionManager; + + constructor(deps: ChangeStatusProformaUseCaseDeps) { + this.finder = deps.finder; + this.updater = deps.updater; + this.fullSnapshotBuilder = deps.fullSnapshotBuilder; + this.transactionManager = deps.transactionManager; } public execute(params: ChangeStatusProformaUseCaseInput) { @@ -29,27 +39,32 @@ export class ChangeStatusProformaUseCase { dto: { new_status }, } = params; - const idOrError = UniqueID.create(proforma_id); - if (idOrError.isFailure) return Result.fail(idOrError.error); + const proformaIdOrError = UniqueID.create(proforma_id); + if (proformaIdOrError.isFailure) { + return Result.fail(proformaIdOrError.error); + } - const proformaId = idOrError.data; + const proformaId = proformaIdOrError.data; return this.transactionManager.complete(async (transaction) => { try { /** 1. Recuperamos la proforma */ - const proformaResult = await this.service.getProformaByIdInCompany( + const proformaResult = await this.finder.findProformaById( companyId, proformaId, transaction ); - if (proformaResult.isFailure) return Result.fail(proformaResult.error); + if (proformaResult.isFailure) { + return Result.fail(proformaResult.error); + } + const proforma = proformaResult.data; /** 2. Hacer el cambio de estado */ const transitionResult = await this.proformaDomainService.transition(proforma, new_status!); if (transitionResult.isFailure) return Result.fail(transitionResult.error); - const updateResult = await this.service.updateProformaStatusByIdInCompany( + const updateResult = await this.updater.updateProformaStatusByIdInCompany( companyId, proformaId, transitionResult.data.status, diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts index 98a50f10..3f04906f 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts @@ -1,4 +1,4 @@ -//export * from "./change-status-proforma.use-case"; +export * from "./change-status-proforma.use-case"; export * from "./create-proforma"; //export * from "./delete-proforma.use-case"; export * from "./get-proforma-by-id.use-case"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts index b505f3a8..92f5474a 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts @@ -1,6 +1,7 @@ import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api"; import { + type ChangeStatusProformaUseCase, type CreateProformaUseCase, type GetProformaByIdUseCase, type IIssuedInvoicePublicServices, @@ -8,6 +9,7 @@ import { type ListProformasUseCase, type ReportProformaUseCase, type UpdateProformaByIdUseCase, + buildChangeStatusProformaUseCase, buildCreateProformaUseCase, buildGetProformaByIdUseCase, buildIssueProformaUseCase, @@ -38,11 +40,12 @@ export type ProformasInternalDeps = { issuedInvoiceServices: IIssuedInvoicePublicServices; }) => IssueProformaUseCase; updateProforma: () => UpdateProformaByIdUseCase; + changeStatusProforma: () => ChangeStatusProformaUseCase; /* deleteProforma: () => DeleteProformaUseCase; - issueProforma: () => IssueProformaUseCase; - changeStatusProforma: () => ChangeStatusProformaUseCase;*/ + + */ }; }; @@ -122,6 +125,14 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter issuer, transactionManager, }), + + changeStatusProforma: () => + buildChangeStatusProformaUseCase({ + finder, + updater, + fullSnapshotBuilder: snapshotBuilders.full, + transactionManager, + }), }, }; } diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/change-status-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/change-status-proforma.controller.ts index c81d54dc..451d71f4 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/change-status-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/change-status-proforma.controller.ts @@ -5,12 +5,14 @@ import { requireCompanyContextGuard, } from "@erp/core/api"; -import type { ChangeStatusProformaByIdRequestDTO } from "../../../../../common/dto/index.ts"; +import type { ChangeStatusProformaByIdRequestDTO } from "../../../../../common/dto"; +import type { ChangeStatusProformaUseCase } from "../../../../application"; +import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; export class ChangeStatusProformaController extends ExpressController { public constructor(private readonly useCase: ChangeStatusProformaUseCase) { super(); - this.errorMapper = customerInvoicesApiErrorMapper; + this.errorMapper = proformasApiErrorMapper; // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query this.registerGuards( @@ -21,7 +23,7 @@ export class ChangeStatusProformaController extends ExpressController { } protected async executeImpl() { - const companyId = this.getTenantId(); // garantizado por tenantGuard + const companyId = this.getTenantId(); if (!companyId) { return this.forbiddenError("Tenant ID not found"); } @@ -32,7 +34,8 @@ export class ChangeStatusProformaController extends ExpressController { } const dto = this.req.body as ChangeStatusProformaByIdRequestDTO; - const result = await this.useCase.execute({ proforma_id, dto, companyId }); + + const result = await this.useCase.execute({ proforma_id, companyId, dto }); return result.match( (data) => this.ok(data), diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts index 398f8976..e8049c3f 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts @@ -5,4 +5,4 @@ export * from "./get-proforma.controller"; export * from "./issue-proforma.controller"; export * from "./list-proformas.controller"; export * from "./report-proforma.controller"; -//export * from "./update-proforma.controller"; +export * from "./update-proforma.controller"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/update-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/update-proforma.controller.ts index 570339f9..4f3d6cf5 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/update-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/update-proforma.controller.ts @@ -5,8 +5,8 @@ import { requireCompanyContextGuard, } from "@erp/core/api"; -import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto/index.ts"; -import type { UpdateProformaByIdUseCase } from "../../../../application/index.ts"; +import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto"; +import type { UpdateProformaByIdUseCase } from "../../../../application"; import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; export class UpdateProformaController extends ExpressController { diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts index 935b278b..d9a305d5 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts @@ -11,6 +11,8 @@ import { } from ".."; import { + ChangeStatusProformaByIdParamsRequestSchema, + ChangeStatusProformaByIdRequestSchema, CreateProformaRequestSchema, GetProformaByIdRequestSchema, IssueProformaByIdParamsRequestSchema, @@ -22,6 +24,7 @@ import { } from "../../../../common"; import type { IIssuedInvoicePublicServices } from "../../../application"; +import { ChangeStatusProformaController } from "./controllers/change-status-proforma.controller"; import { CreateProformaController } from "./controllers/create-proforma.controller"; import { UpdateProformaController } from "./controllers/update-proforma.controller"; @@ -130,8 +133,6 @@ export const proformasRouter = (params: StartParams) => { } );*/ - /* - router.patch( "/:proforma_id/status", //checkTabContext, @@ -140,11 +141,11 @@ export const proformasRouter = (params: StartParams) => { validateRequest(ChangeStatusProformaByIdRequestSchema, "body"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.changeStatus_proforma(); + const useCase = deps.useCases.changeStatusProforma(); const controller = new ChangeStatusProformaController(useCase); return controller.execute(req, res, next); } - );*/ + ); router.put( "/:proforma_id/issue", diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts index 20abc09b..8746a126 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts @@ -351,9 +351,9 @@ export class ProformaRepository searchableFields: ["invoice_number", "reference", "description"], mappings: { invoice_date: "invoice_date", - invoice_number: "invoice_number", - reference: "reference", - description: "description", + invoice_number: "CustomerInvoiceModel.invoice_number", + reference: "CustomerInvoiceModel.reference", + description: "CustomerInvoiceModel.description", recipient_name: "current_customer.name", }, sortableFields: [ diff --git a/modules/customer-invoices/src/web/customer-invoice-routes.tsx b/modules/customer-invoices/src/web/customer-invoice-routes.tsx index cbf35ab1..2b14b241 100644 --- a/modules/customer-invoices/src/web/customer-invoice-routes.tsx +++ b/modules/customer-invoices/src/web/customer-invoice-routes.tsx @@ -36,7 +36,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] ), children: [ - { path: "", index: true, element: }, // index + { index: true, element: }, // index { path: "list", element: }, //{ path: "create", element: }, { path: ":id/edit", element: }, diff --git a/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx b/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx index a7b1dada..ab77d5f0 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/pages/list-proformas-page.tsx @@ -1,8 +1,10 @@ import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components"; import { useReturnToNavigation } from "@erp/core/hooks"; -import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components"; +import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components"; import { Button, + Card, + CardContent, ResizableHandle, ResizablePanel, ResizablePanelGroup, @@ -12,7 +14,14 @@ import { SelectTrigger, SelectValue, } from "@repo/shadcn-ui/components"; -import { FilterIcon, PlusIcon } from "lucide-react"; +import { + CheckCircle2Icon, + CircleDollarSignIcon, + ClockIcon, + FileTextIcon, + FilterIcon, + PlusIcon, +} from "lucide-react"; import { createSearchParams, useLocation, useNavigate } from "react-router-dom"; import { useTranslation } from "../../../../i18n"; @@ -152,102 +161,171 @@ export const ListProformasPage = () => { } return ( -
- - navigate("/proformas/create")} - size={"default"} - > - - {t("pages.proformas.create.title")} - - } - title={t("pages.proformas.list.title")} - /> - - - - {isPanelOpen ? ( - + {/* Header */} + navigate("/proformas/create")} + size={"default"} > - - {listContent} - + + {t("pages.proformas.create.title")} + + } + title={t("pages.proformas.list.title")} + /> - - - -
- handleEditClick(proforma.id)} - onOpenChange={(open) => { - if (!open) { - panelCtrl.closePanel(); - return; - } - - panelCtrl.panelState.onOpenChange(true); - }} - open={panelCtrl.panelState.isOpen} - proforma={panelCtrl.proforma} - visibility={panelCtrl.panelState.visibility} - /> + {/* Stats Cards */} +
+ + +
+
+
- - - ) : ( -
{listContent}
- )} - <> - {/* Issue */} - { - if (!open) { - issueDialogCtrl.closeDialog(); - } - }} - open={issueDialogCtrl.open} - target={issueDialogCtrl.target} - /> +
+

Total proformas

+

1.248

+

+ ↑ 12.5% vs. mes anterior +

+
+
+
+
- { - if (!open) { - changeStatusDialogCtrl.closeDialog(); - } - }} - open={changeStatusDialogCtrl.open} - targets={changeStatusDialogCtrl.targets} - /> + + +
+
+ +
+
+

Importe total

+

125.430,75 €

+

+ ↑ 8.1% vs. mes anterior +

+
+
+
+
- {/* Eliminar */} - { - if (!open) { - deleteDialogCtrl.closeDialog(); - } - }} - open={deleteDialogCtrl.open} - targets={deleteDialogCtrl.targets} - /> - - -
+ + +
+
+ +
+
+

Pendientes

+

356

+

+ ↑ 5.2% vs. mes anterior +

+
+
+
+
+ + + +
+
+ +
+
+

Convertidas

+

892

+

+ ↑ 15.7% vs. mes anterior +

+
+
+
+
+ + + {/* Table */} + {isPanelOpen ? ( + + + {listContent} + + + + + +
+ handleEditClick(proforma.id)} + onOpenChange={(open) => { + if (!open) { + panelCtrl.closePanel(); + return; + } + + panelCtrl.panelState.onOpenChange(true); + }} + open={panelCtrl.panelState.isOpen} + proforma={panelCtrl.proforma} + visibility={panelCtrl.panelState.visibility} + /> +
+
+
+ ) : ( +
{listContent}
+ )} + <> + {/* Issue */} + { + if (!open) { + issueDialogCtrl.closeDialog(); + } + }} + open={issueDialogCtrl.open} + target={issueDialogCtrl.target} + /> + + { + if (!open) { + changeStatusDialogCtrl.closeDialog(); + } + }} + open={changeStatusDialogCtrl.open} + targets={changeStatusDialogCtrl.targets} + /> + + {/* Eliminar */} + { + if (!open) { + deleteDialogCtrl.closeDialog(); + } + }} + open={deleteDialogCtrl.open} + targets={deleteDialogCtrl.targets} + /> + + ); }; diff --git a/modules/customer-invoices/src/web/proformas/shared/ui/blocks/proforma-layout.tsx b/modules/customer-invoices/src/web/proformas/shared/ui/blocks/proforma-layout.tsx index 4bedf671..5fd6fe15 100644 --- a/modules/customer-invoices/src/web/proformas/shared/ui/blocks/proforma-layout.tsx +++ b/modules/customer-invoices/src/web/proformas/shared/ui/blocks/proforma-layout.tsx @@ -1,5 +1,9 @@ -import type { PropsWithChildren } from "react"; +import type { ReactNode } from "react"; -export const ProformaLayout = ({ children }: PropsWithChildren) => { - return
{children}
; -}; +interface ProformaLayoutProps { + children: ReactNode; +} + +export function ProformaLayout({ children }: ProformaLayoutProps) { + return
{children}
; +} diff --git a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-compact-totals.tsx b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-compact-totals.tsx index 2f86d27d..1cc9c79a 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-compact-totals.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-compact-totals.tsx @@ -61,12 +61,12 @@ export const ProformaCompactTotals = ({ }; return ( - + {/* Panel expandible con desglose */} @@ -176,15 +176,20 @@ export const ProformaCompactTotals = ({ {/* Barra principal de totales - siempre visible */} - + {/* Boton expandir */} - {/* Totales en linea */} -
+
{t("proformas.update.totals.subtotalBeforeDiscounts", "Subtotal")} diff --git a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-header.tsx b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-header.tsx index 973e1900..a9a7c06d 100644 --- a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-header.tsx +++ b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-header.tsx @@ -1,97 +1,136 @@ +// modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-header.tsx + +import { PageFormHeader, PageKeyboardShortcutsButton } from "@erp/core/components"; import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@repo/shadcn-ui/components"; -import { ArrowLeftIcon, KeyboardIcon, MoreHorizontalIcon, SaveIcon, XIcon } from "lucide-react"; + CancelActionButton, + FormActionsBar, + type FormSecondaryAction, + FormSecondaryActionsMenu, + RhfSubmitActionButton, +} from "@repo/rdx-ui/components"; +import type { ReactNode } from "react"; + +export interface ProformaUpdateHeaderLabels { + title: string; + back: string; + modified: string; + cancel: string; + save: string; + saving: string; + moreActions: string; + keyboardShortcuts: string; + duplicate: string; + exportPdf: string; + delete: string; +} export interface ProformaUpdateHeaderProps { - onSave?: () => void; + formId?: string; + + labels: ProformaUpdateHeaderLabels; + onCancel?: () => void; + onDuplicate?: () => void; + onExportPdf?: () => void; + onDelete?: () => void; disabled?: boolean; readOnly?: boolean; + isSaving?: boolean; hasChanges?: boolean; + + className?: string; + children?: ReactNode; } export const ProformaUpdateHeader = ({ - onSave, + formId, + labels, onCancel, + onDuplicate, + onExportPdf, + onDelete, disabled = false, readOnly = false, + isSaving = false, hasChanges = false, + className, + children, }: ProformaUpdateHeaderProps) => { + const computedDisabled = disabled || isSaving; + + const secondaryActions: FormSecondaryAction[] = [ + /*{ + id: "duplicate", + label: labels.duplicate, + icon: