diff --git a/client/src/Routes.tsx b/client/src/Routes.tsx index eac1139..3fee279 100644 --- a/client/src/Routes.tsx +++ b/client/src/Routes.tsx @@ -2,6 +2,7 @@ import { Outlet, RouterProvider, createBrowserRouter } from "react-router-dom"; import { DealerLayout, DealersList, + ErrorPage, LoginPage, LogoutPage, QuoteCreate, @@ -25,6 +26,13 @@ export const Routes = () => { }, ]; + const routesForErrors = [ + { + path: "*", + Component: ErrorPage, + }, + ]; + // Define routes accessible only to authenticated users const routesForAuthenticatedOnly = [ { @@ -123,7 +131,12 @@ export const Routes = () => { // Combine and conditionally include routes based on authentication status const router = createBrowserRouter( - [...routesForPublic, ...routesForAuthenticatedOnly, ...routesForNotAuthenticatedOnly], + [ + ...routesForPublic, + ...routesForAuthenticatedOnly, + ...routesForNotAuthenticatedOnly, + ...routesForErrors, + ], { //basename: "/app", } diff --git a/client/src/app/ErrorPage.tsx b/client/src/app/ErrorPage.tsx index f8f38e6..ac753a3 100644 --- a/client/src/app/ErrorPage.tsx +++ b/client/src/app/ErrorPage.tsx @@ -25,6 +25,29 @@ export const ErrorPage = (props: ErrorPageProps) => { try again.

); + + return ( +
+
+
+

+ Oops, page not found! +

+

+ The page you're looking for doesn't exist or has been moved. +

+
+ +
+
+
+ ); + return (
{msg} diff --git a/client/src/app/quotes/components/AppendCatalogArticleRowButton.tsx b/client/src/app/quotes/components/AppendCatalogArticleRowButton.tsx new file mode 100644 index 0000000..d675286 --- /dev/null +++ b/client/src/app/quotes/components/AppendCatalogArticleRowButton.tsx @@ -0,0 +1,31 @@ +import { cn } from "@/lib/utils"; +import { Button, ButtonProps } from "@/ui"; +import { t } from "i18next"; +import { PackagePlusIcon } from "lucide-react"; + +export interface AppendCatalogArticleRowButtonProps extends ButtonProps { + label?: string; + className?: string; +} + +export const AppendCatalogArticleRowButton = ({ + label = t("common.append_article"), + className, + ...props +}: AppendCatalogArticleRowButtonProps): JSX.Element => ( + +); + +AppendCatalogArticleRowButton.displayName = "AddNewRowButton"; diff --git a/client/src/app/quotes/components/AddNewRowButton.tsx b/client/src/app/quotes/components/AppendEmptyRowButton.tsx similarity index 65% rename from client/src/app/quotes/components/AddNewRowButton.tsx rename to client/src/app/quotes/components/AppendEmptyRowButton.tsx index a314d35..87a2e16 100644 --- a/client/src/app/quotes/components/AddNewRowButton.tsx +++ b/client/src/app/quotes/components/AppendEmptyRowButton.tsx @@ -1,17 +1,18 @@ import { cn } from "@/lib/utils"; import { Button, ButtonProps } from "@/ui"; +import { t } from "i18next"; import { PlusCircleIcon } from "lucide-react"; -export interface AddNewRowButtonProps extends ButtonProps { +export interface AppendEmptyRowButtonProps extends ButtonProps { label?: string; className?: string; } -export const AddNewRowButton = ({ - label = "Añade nueva fila", +export const AppendEmptyRowButton = ({ + label = t("common.append_empty_row"), className, ...props -}: AddNewRowButtonProps): JSX.Element => ( +}: AppendEmptyRowButtonProps): JSX.Element => ( + + {t("common.duplicate_selected_rows_tooltip")} + + + + + + {t("common.remove_selected_rows_tooltip")} + + + + + + + {t("common.reset_selected_rows_tooltip")} + + +

{t("common.rows_selected", { count: selectedRowsCount })}

+
+ + + ); + } + + return ( + ); }; diff --git a/client/src/app/quotes/components/editors/CatalogPickerDialog.tsx b/client/src/app/quotes/components/editors/CatalogPickerDialog.tsx new file mode 100644 index 0000000..42d95e0 --- /dev/null +++ b/client/src/app/quotes/components/editors/CatalogPickerDialog.tsx @@ -0,0 +1,28 @@ +import { Button, Dialog, DialogContent, DialogFooter } from "@/ui"; + +import { DataTableProvider } from "@/lib/hooks"; +import { CatalogPickerDataTable } from "../CatalogPickerDataTable"; + +export const CatalogPickerDialog = ({ + isOpen, + onOpenChange, + onSelect, +}: { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onSelect: (data: unknown) => void; +}) => { + return ( + + + + + + + + + + + + ); +}; diff --git a/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx b/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx index e75e574..5617d34 100644 --- a/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx +++ b/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx @@ -4,6 +4,7 @@ import { FormQuantityField, FormTextAreaField, } from "@/components"; + import { DataTableProvider } from "@/lib/hooks"; import { cn } from "@/lib/utils"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/ui"; @@ -13,7 +14,9 @@ import { t } from "i18next"; import { useCallback, useState } from "react"; import { useFieldArray, useFormContext } from "react-hook-form"; import { useDetailColumns } from "../../hooks"; +import { CatalogPickerDataTable } from "../CatalogPickerDataTable"; import { QuoteItemsSortableDataTable } from "../QuoteItemsSortableDataTable"; +import { CatalogPickerDialog } from "./CatalogPickerDialog"; export const QuoteDetailsCardEditor = ({ currency, @@ -26,6 +29,10 @@ export const QuoteDetailsCardEditor = ({ }) => { const { control, register } = useFormContext(); + const [pickerMode] = useState<"dialog" | "panel">("dialog"); + + const [pickerDialogOpen, setPickerDialogOpen] = useState(false); + const { fields, ...fieldActions } = useFieldArray({ control, name: "items", @@ -192,15 +199,15 @@ export const QuoteDetailsCardEditor = ({ } ); - const handleInsertArticle = useCallback( - (newArticle: any) => { + const handleAppendCatalogArticle = useCallback( + (article: any) => { fieldActions.append({ - ...newArticle, + ...article, quantity: { amount: 100, scale: Quantity.DEFAULT_SCALE, }, - unit_price: newArticle.retail_price, + unit_price: article.retail_price, }); }, [fieldActions] @@ -211,24 +218,26 @@ export const QuoteDetailsCardEditor = ({ const defaultLayout = [265, 440, 655]; const navCollapsedSize = 4; - return ( - <> - - - - ); + if (pickerMode === "dialog") { + return ( +
+ setPickerDialogOpen(true), + }} + columns={columns} + data={fields} + defaultValues={defaultValues} + /> + +
+ ); + } return ( - + - + diff --git a/client/src/components/DataTable/DataTable.tsx b/client/src/components/DataTable/DataTable.tsx index 179fb2b..7877d7e 100644 --- a/client/src/components/DataTable/DataTable.tsx +++ b/client/src/components/DataTable/DataTable.tsx @@ -26,7 +26,7 @@ export type DataTableColumnProps = ColumnDef; export type DataTablePaginationOptionsProps = Pick< DataTablePaginationProps, - "visible" + "visible" | "enablePageSizeSelector" >; export type DataTableHeaderOptionsProps = { @@ -41,6 +41,8 @@ export type DataTableProps = PropsWithChildren<{ paginationOptions?: DataTablePaginationOptionsProps; headerOptions?: DataTableHeaderOptionsProps; className?: string; + contentClassName?: string; + footerClassName?: string; rowClassName?: string; cellClassName?: string; }>; @@ -54,6 +56,8 @@ export function DataTable({ headerOptions = { visible: true }, children, className, + contentClassName, + footerClassName, rowClassName, cellClassName, }: DataTableProps) { @@ -67,7 +71,7 @@ export function DataTable({ {description} )} - + {children && ( <>
{children}
@@ -124,12 +128,8 @@ export function DataTable({
- - + + ); diff --git a/client/src/components/DataTable/DataTablePagination.tsx b/client/src/components/DataTable/DataTablePagination.tsx index adda93d..c46280e 100644 --- a/client/src/components/DataTable/DataTablePagination.tsx +++ b/client/src/components/DataTable/DataTablePagination.tsx @@ -15,12 +15,14 @@ import { useMemo } from "react"; export type DataTablePaginationProps = { table: Table; className?: string; + enablePageSizeSelector?: boolean; visible?: boolean | "auto"; }; export function DataTablePagination({ table, className, + enablePageSizeSelector = true, visible = "auto", }: DataTablePaginationProps) { const isVisible = useMemo(() => visible === true, [visible]); @@ -31,11 +33,11 @@ export function DataTablePagination({ } return ( -
+
{table.getSelectedRowModel().rows.length > 0 && ( <> - {t("common.rows_selected", { + {t("common.rows_selected_of_total", { count: table.getFilteredSelectedRowModel().rows.length, total: table.getFilteredRowModel().rows.length, })} @@ -43,75 +45,86 @@ export function DataTablePagination({ )}
-
-
-

{t("common.rows_per_page")}

+
+ {enablePageSizeSelector && ( +
+

{t("common.rows_per_page")}

- -
-
- {t("common.num_page_of_total", { - count: table.getState().pagination.pageIndex + 1, - total: table.getPageCount(), - })} -
-
- - - - + +
+ )} +
+
+

+ {t("common.num_page_of_total", { + count: table.getState().pagination.pageIndex + 1, + total: table.getPageCount(), + })} +

+
+
+ + + + +
diff --git a/client/src/lib/hooks/useDataTable/DataTableContext.tsx b/client/src/lib/hooks/useDataTable/DataTableContext.tsx index 49fc4bb..a7526a8 100644 --- a/client/src/lib/hooks/useDataTable/DataTableContext.tsx +++ b/client/src/lib/hooks/useDataTable/DataTableContext.tsx @@ -27,12 +27,20 @@ export const DataTableContext = createContext(nul export const DataTableProvider = ({ syncWithLocation = true, initialGlobalFilter = "", + initialPageIndex, + initialPageSize, children, }: PropsWithChildren<{ syncWithLocation?: boolean; initialGlobalFilter?: string; + initialPageIndex?: number; + initialPageSize?: number; }>) => { - const [pagination, setPagination] = useSyncedPagination(syncWithLocation); + const [pagination, setPagination] = useSyncedPagination({ + syncWithLocation, + initialPageIndex, + initialPageSize, + }); const [globalFilter, setGlobalFilter] = useState(initialGlobalFilter); const [sorting, setSorting] = useState([]); diff --git a/client/src/lib/hooks/useDataTable/useSyncedPagination.tsx b/client/src/lib/hooks/useDataTable/useSyncedPagination.tsx index 85a9153..2ac5b09 100644 --- a/client/src/lib/hooks/useDataTable/useSyncedPagination.tsx +++ b/client/src/lib/hooks/useDataTable/useSyncedPagination.tsx @@ -1,8 +1,21 @@ import { usePagination, usePaginationSyncWithLocation } from "../usePagination"; -export const useSyncedPagination = (syncWithLocation: boolean) => { +type UseSyncedPaginationProps = { + syncWithLocation?: boolean; + initialPageIndex?: number; + initialPageSize?: number; +}; + +export const useSyncedPagination = ({ + syncWithLocation = true, + initialPageIndex, + initialPageSize, +}: UseSyncedPaginationProps) => { const [paginationWithLocation, setPaginationWithLocation] = usePaginationSyncWithLocation(); - const [paginationWithoutLocation, setPaginationWithoutLocation] = usePagination(); + const [paginationWithoutLocation, setPaginationWithoutLocation] = usePagination( + initialPageIndex, + initialPageSize + ); if (syncWithLocation) { return [paginationWithLocation, setPaginationWithLocation] as const; diff --git a/client/src/lib/hooks/usePagination/usePagination.tsx b/client/src/lib/hooks/usePagination/usePagination.tsx index 393fc5f..6d825a2 100644 --- a/client/src/lib/hooks/usePagination/usePagination.tsx +++ b/client/src/lib/hooks/usePagination/usePagination.tsx @@ -7,7 +7,7 @@ import { import { useState } from "react"; -export const DEFAULT_PAGE_SIZES = [15, 30, 50, 75, 100]; +export const DEFAULT_PAGE_SIZES = [5, 10, 15, 30, 50, 75, 100]; export interface PaginationState { pageIndex: number; diff --git a/client/src/lib/hooks/useUnsavedChangesNotifier/WarnAboutChangeContext.tsx b/client/src/lib/hooks/useUnsavedChangesNotifier/WarnAboutChangeContext.tsx index 80a0a66..90319f6 100644 --- a/client/src/lib/hooks/useUnsavedChangesNotifier/WarnAboutChangeContext.tsx +++ b/client/src/lib/hooks/useUnsavedChangesNotifier/WarnAboutChangeContext.tsx @@ -1,6 +1,5 @@ -import { CustomDialog } from "@/components"; import { NullOr } from "@shared/utilities"; -import { PropsWithChildren, createContext, useCallback, useMemo, useState } from "react"; +import { createContext } from "react"; import { UnsavedChangesNotifierProps } from "./useUnsavedChangesNotifier"; export interface IUnsavedWarnContextState { @@ -8,48 +7,3 @@ export interface IUnsavedWarnContextState { } export const UnsavedWarnContext = createContext>(null); - -export const UnsavedWarnProvider = ({ children }: PropsWithChildren) => { - const [confirm, setConfirm] = useState>(null); - - const [open, toggle] = useState(false); - - const show = useCallback( - (confirmOptions: NullOr) => { - setConfirm(confirmOptions); - toggle(true); - }, - [toggle, setConfirm] - ); - - const onConfirm = () => { - confirm?.onConfirm?.(); - toggle(false); - }; - - const onCancel = () => { - confirm?.onCancel?.(); - toggle(false); - }; - - const value = useMemo(() => ({ show }), [show]); - - return ( - - {children} - { - console.log("onCancel"); - onCancel(); - }} - onConfirm={() => onConfirm()} - title={confirm?.title} - description={confirm?.subtitle} - confirmLabel={confirm?.confirmText} - cancelLabel={confirm?.cancelText} - isOpen={open} - /> - - ); -}; diff --git a/client/src/lib/hooks/useUnsavedChangesNotifier/WarnAboutChangeProvider.tsx b/client/src/lib/hooks/useUnsavedChangesNotifier/WarnAboutChangeProvider.tsx new file mode 100644 index 0000000..197730a --- /dev/null +++ b/client/src/lib/hooks/useUnsavedChangesNotifier/WarnAboutChangeProvider.tsx @@ -0,0 +1,50 @@ +import { CustomDialog } from "@/components"; +import { NullOr } from "@shared/utilities"; +import { PropsWithChildren, useCallback, useMemo, useState } from "react"; +import { UnsavedChangesNotifierProps } from "./useUnsavedChangesNotifier"; +import { UnsavedWarnContext } from "./WarnAboutChangeContext"; + +export const UnsavedWarnProvider = ({ children }: PropsWithChildren) => { + const [confirm, setConfirm] = useState>(null); + + const [open, toggle] = useState(false); + + const show = useCallback( + (confirmOptions: NullOr) => { + setConfirm(confirmOptions); + toggle(true); + }, + [toggle, setConfirm] + ); + + const onConfirm = () => { + confirm?.onConfirm?.(); + toggle(false); + }; + + const onCancel = () => { + confirm?.onCancel?.(); + toggle(false); + }; + + const value = useMemo(() => ({ show }), [show]); + + return ( + + {children} + { + console.log("onCancel"); + onCancel(); + }} + onConfirm={() => onConfirm()} + title={confirm?.title} + description={confirm?.subtitle} + confirmLabel={confirm?.confirmText} + cancelLabel={confirm?.cancelText} + isOpen={open} + /> + + ); +}; diff --git a/client/src/lib/hooks/useUnsavedChangesNotifier/index.ts b/client/src/lib/hooks/useUnsavedChangesNotifier/index.ts index 3d00b5e..6bb164e 100644 --- a/client/src/lib/hooks/useUnsavedChangesNotifier/index.ts +++ b/client/src/lib/hooks/useUnsavedChangesNotifier/index.ts @@ -1,3 +1,4 @@ -export * from "./WarnAboutChangeContext"; export * from "./useUnsavedChangesNotifier"; export * from "./useWarnAboutChange"; +export * from "./WarnAboutChangeContext"; +export * from "./WarnAboutChangeProvider"; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index d4f1c08..750b13e 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -17,7 +17,8 @@ "sort_desc": "Desc", "sort_desc_description": "In descending order. Click to sort in ascending order.", "sort_none_description": "No sorting order. Click to sort in ascending order.", - "rows_selected": "{{count}} of {{total}} row(s) selected.", + "rows_selected": "{{count}} row(s) selected.", + "rows_selected_of_total": "{{count}} of {{total}} row(s) selected.", "rows_per_page": "Rows per page", "num_page_of_total": "Page {{count}} of {{total}}", "go_to_first_page": "Go to first page", @@ -29,13 +30,17 @@ "error": "Error", "actions": "Actions", "open_menu": "Open menu", - "duplicate_rows": "Duplicate", - "duplicate_rows_tooltip": "Duplicate selected row(s)", + "duplicate_selected_rows": "Duplicate", + "duplicate_selected_rows_tooltip": "Duplicate selected row(s)", "append_empty_row": "Append row", "append_empty_row_tooltip": "Append a empty row", "append_article": "Append article", "append_article_tooltip": "Select and add an item from the catalog", "remove_row": "Remove", + "remove_selected_rows": "Remove", + "remove_selected_rows_tooltip": "Remove selected row(s)", + "reset_selected_rows": "Reset selection", + "reset_selected_rows_tooltip": "Reset selected row(s)", "insert_row_above": "Insert row above", "insert_row_below": "Insert row below", "pick_date": "Select a date", diff --git a/client/src/locales/es.json b/client/src/locales/es.json index 4722395..a0180cf 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -17,7 +17,8 @@ "sort_desc": "Desc", "sort_desc_description": "En orden descendente. Click para ordenar ascendentemente.", "sort_none_description": "Sin orden. Click para ordenar ascendentemente.", - "rows_selected": "{{count}} de {{total}} fila(s) seleccionadas.", + "rows_selected": "{{count}} fila(s) seleccionadas.", + "rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas.", "rows_per_page": "Filas por página", "num_page_of_total": "Página {{count}} de {{total}}", "go_to_first_page": "Ir a la primera página", @@ -29,13 +30,17 @@ "error": "Error", "actions": "Acciones", "open_menu": "Abrir el menú", - "duplicate_rows": "Duplicar", - "duplicate_rows_tooltip": "Duplica las fila(s) seleccionadas(s)", + "duplicate_selected_rows": "Duplicar", + "duplicate_selected_rows_tooltip": "Duplica las fila(s) seleccionadas(s)", "append_empty_row": "Añadir fila", "append_empty_row_tooltip": "Añadir una fila vacía", "append_article": "Añadir artículo", "append_article_tooltip": "Elegir un artículo del catálogo y añadirlo", "remove_row": "Eliminar", + "remove_selected_rows": "Eliminar", + "remove_selected_rows_tooltip": "Elimina las fila(s) seleccionadas(s)", + "reset_selected_rows": "Quitar selection", + "reset_selected_rows_tooltip": "Dejar de seleccionar la(s) fila(s)", "insert_row_above": "Insertar fila encima", "insert_row_below": "Insertar fila debajo", "pick_date": "Elige una fecha", diff --git a/shared/lib/contexts/common/domain/entities/QueryCriteria/Pagination/defaults.ts b/shared/lib/contexts/common/domain/entities/QueryCriteria/Pagination/defaults.ts index a826c01..9c0efc8 100644 --- a/shared/lib/contexts/common/domain/entities/QueryCriteria/Pagination/defaults.ts +++ b/shared/lib/contexts/common/domain/entities/QueryCriteria/Pagination/defaults.ts @@ -1,5 +1,5 @@ export const INITIAL_PAGE_INDEX = 0; -export const INITIAL_PAGE_SIZE = 15; +export const INITIAL_PAGE_SIZE = 5; export const MIN_PAGE_INDEX = 0; export const MIN_PAGE_SIZE = 1;