From e6498b41048ed879e2633b944fb69d0883da7b75 Mon Sep 17 00:00:00 2001 From: David Arranz Date: Tue, 11 Jun 2024 18:48:09 +0200 Subject: [PATCH] . --- client/jest.config.ts | 13 + client/package.json | 15 +- client/src/Routes.tsx | 27 +- client/src/app/catalog/CatalogActions.ts | 54 ++ client/src/app/catalog/CatalogContext.tsx | 21 + .../catalog/components/CatalogDataTable.tsx | 36 + client/src/app/catalog/components/index.ts | 1 + .../components/useCatalogTableColumns.tsx | 77 ++ client/src/app/catalog/hooks/index.ts | 1 + .../src/app/catalog/hooks/useCatalogList.tsx | 40 + client/src/app/catalog/index.ts | 1 + client/src/app/catalog/layout.tsx | 17 + client/src/app/catalog/list.tsx | 654 ++++++++------- client/src/app/catalog/useCatalogContext.tsx | 8 + client/src/app/dashboard/index.tsx | 748 +++++++++--------- client/src/app/dealers/index.ts | 1 + client/src/app/dealers/layout.tsx | 11 + client/src/app/dealers/list.tsx | 311 +++++++- .../components/ButtonGroup/ButtonGroup.tsx | 14 + client/src/components/ButtonGroup/index.ts | 1 + .../CustomButtons/BackHistoryButton.tsx | 31 + client/src/components/CustomButtons/index.ts | 1 + client/src/components/DataTable/DataTable.tsx | 97 +++ .../DataTable/DataTableColumnHeader.tsx | 123 +++ .../DataTable/DataTableFacetedFilter.tsx | 149 ++++ .../DataTable/DataTablePagination.tsx | 102 +++ .../DataTable/DataTableRowActions.tsx | 75 ++ .../DataTable/DataTableRowDragHandleCell.tsx | 30 + .../DataTable/DataTableSelectionColumn.tsx | 36 + .../DataTable/DataTableSkeleton.tsx | 149 ++++ .../DataTableColumnOptions.tsx | 57 ++ .../DataTableToolbar/DataTableToolbar.tsx | 138 ++++ .../DataTable/DataTableToolbar/index.ts | 1 + client/src/components/DataTable/index.ts | 6 + .../EmptyState/SimpleEmptyState.tsx | 42 + client/src/components/EmptyState/index.ts | 1 + .../ErrorOverlay/ErrorOverlay.module.css | 20 + .../components/ErrorOverlay/ErrorOverlay.tsx | 53 ++ client/src/components/ErrorOverlay/index.ts | 1 + .../SorteableDataTable/AddNewRowButton.tsx | 30 + .../SorteableDataTable/SortableDataTable.tsx | 463 +++++++++++ .../SortableDataTableToolbar.tsx | 212 +++++ .../SorteableDataTable/SortableTableRow.tsx | 80 ++ .../components/SorteableDataTable/index.tsx | 1 + client/src/components/index.ts | 6 + client/src/lib/hooks/index.ts | 4 +- .../hooks/useDataSource/DataSourceContext.tsx | 2 +- .../hooks/useDataTable/DataTableContext.tsx | 21 + client/src/lib/hooks/useDataTable/index.ts | 2 + client/src/lib/hooks/useDataTable/types.ts | 12 + .../lib/hooks/useDataTable/useDataTable.tsx | 22 +- .../useDataTable/useDataTableContext.tsx | 9 + .../hooks/usePagination/usePagination.test.ts | 75 ++ .../lib/hooks/usePagination/usePagination.tsx | 22 +- .../usePagination/usePaginationParams.tsx | 44 +- .../hooks/useTranslation/useTranslation.tsx | 0 client/src/locales/en.json | 34 +- client/src/locales/es.json | 8 +- .../useTranslation/index.ts => setupTests.js} | 0 59 files changed, 3506 insertions(+), 704 deletions(-) create mode 100644 client/jest.config.ts create mode 100644 client/src/app/catalog/CatalogActions.ts create mode 100644 client/src/app/catalog/CatalogContext.tsx create mode 100644 client/src/app/catalog/components/CatalogDataTable.tsx create mode 100644 client/src/app/catalog/components/index.ts create mode 100644 client/src/app/catalog/components/useCatalogTableColumns.tsx create mode 100644 client/src/app/catalog/hooks/index.ts create mode 100644 client/src/app/catalog/hooks/useCatalogList.tsx create mode 100644 client/src/app/catalog/layout.tsx create mode 100644 client/src/app/catalog/useCatalogContext.tsx create mode 100644 client/src/app/dealers/layout.tsx create mode 100644 client/src/components/ButtonGroup/ButtonGroup.tsx create mode 100644 client/src/components/ButtonGroup/index.ts create mode 100644 client/src/components/CustomButtons/BackHistoryButton.tsx create mode 100644 client/src/components/CustomButtons/index.ts create mode 100644 client/src/components/DataTable/DataTable.tsx create mode 100644 client/src/components/DataTable/DataTableColumnHeader.tsx create mode 100644 client/src/components/DataTable/DataTableFacetedFilter.tsx create mode 100644 client/src/components/DataTable/DataTablePagination.tsx create mode 100644 client/src/components/DataTable/DataTableRowActions.tsx create mode 100644 client/src/components/DataTable/DataTableRowDragHandleCell.tsx create mode 100644 client/src/components/DataTable/DataTableSelectionColumn.tsx create mode 100644 client/src/components/DataTable/DataTableSkeleton.tsx create mode 100644 client/src/components/DataTable/DataTableToolbar/DataTableColumnOptions.tsx create mode 100644 client/src/components/DataTable/DataTableToolbar/DataTableToolbar.tsx create mode 100644 client/src/components/DataTable/DataTableToolbar/index.ts create mode 100644 client/src/components/DataTable/index.ts create mode 100644 client/src/components/EmptyState/SimpleEmptyState.tsx create mode 100644 client/src/components/EmptyState/index.ts create mode 100644 client/src/components/ErrorOverlay/ErrorOverlay.module.css create mode 100644 client/src/components/ErrorOverlay/ErrorOverlay.tsx create mode 100644 client/src/components/ErrorOverlay/index.ts create mode 100644 client/src/components/SorteableDataTable/AddNewRowButton.tsx create mode 100644 client/src/components/SorteableDataTable/SortableDataTable.tsx create mode 100644 client/src/components/SorteableDataTable/SortableDataTableToolbar.tsx create mode 100644 client/src/components/SorteableDataTable/SortableTableRow.tsx create mode 100644 client/src/components/SorteableDataTable/index.tsx create mode 100644 client/src/lib/hooks/useDataTable/DataTableContext.tsx create mode 100644 client/src/lib/hooks/useDataTable/types.ts create mode 100644 client/src/lib/hooks/useDataTable/useDataTableContext.tsx create mode 100644 client/src/lib/hooks/usePagination/usePagination.test.ts delete mode 100644 client/src/lib/hooks/useTranslation/useTranslation.tsx rename client/src/{lib/hooks/useTranslation/index.ts => setupTests.js} (100%) diff --git a/client/jest.config.ts b/client/jest.config.ts new file mode 100644 index 0000000..3b083db --- /dev/null +++ b/client/jest.config.ts @@ -0,0 +1,13 @@ +// jest.config.ts + +export default { + preset: "ts-jest", + testEnvironment: "jest-environment-jsdom", + transform: { + "^.+\\.tsx?$": "ts-jest", + // process `*.tsx` files with `ts-jest` + }, + moduleNameMapper: { + "\\.(gif|ttf|eot|svg|png)$": "/test/__ mocks __/fileMock.js", + }, +}; diff --git a/client/package.json b/client/package.json index e14eea9..e9b4a6c 100644 --- a/client/package.json +++ b/client/package.json @@ -8,9 +8,12 @@ "dev": "vite --host", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "preview": "vite preview", + "test": "jest" }, "dependencies": { + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", "@hookform/resolvers": "^3.5.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", @@ -40,6 +43,7 @@ "@radix-ui/react-toggle-group": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-query": "^5.39.0", + "@tanstack/react-table": "^8.17.3", "axios": "^1.7.2", "class-variance-authority": "^0.7.0", "cmdk": "^1.0.0", @@ -62,6 +66,10 @@ }, "devDependencies": { "@tanstack/react-query-devtools": "^5.39.0", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^16.0.0", + "@testing-library/react-hooks": "^8.0.1", + "@types/jest": "^29.5.12", "@types/node": "^20.14.0", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", @@ -73,10 +81,15 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.6", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "postcss": "^8.4.38", "postcss-import": "^16.1.0", "tailwind-merge": "^2.3.0", "tailwindcss": "^3.4.3", + "ts-jest": "^29.1.4", + "ts-node": "^10.9.2", "typescript": "^5.2.2", "vite": "^5.2.12" } diff --git a/client/src/Routes.tsx b/client/src/Routes.tsx index cadcdd5..c74b908 100644 --- a/client/src/Routes.tsx +++ b/client/src/Routes.tsx @@ -1,8 +1,7 @@ -import { RouterProvider, createBrowserRouter } from "react-router-dom"; -import { LoginPage, LogoutPage, SettingsPage, StartPage } from "./app"; -import { CatalogList } from "./app/catalog"; +import { Outlet, RouterProvider, createBrowserRouter } from "react-router-dom"; +import { DealerLayout, DealersList, LoginPage, LogoutPage, SettingsPage, StartPage } from "./app"; +import { CatalogLayout, CatalogList } from "./app/catalog"; import { DashboardPage } from "./app/dashboard"; -import { DealersList } from "./app/dealers/list"; import { QuotesList } from "./app/quotes/list"; import { ProtectedRoute } from "./components"; @@ -29,17 +28,33 @@ export const Routes = () => { path: "/catalog", element: ( - + + + ), + children: [ + { + index: true, + element: , + }, + ], }, { path: "/dealers", element: ( - + + + ), + children: [ + { + index: true, + element: , + }, + ], }, { path: "/quotes", diff --git a/client/src/app/catalog/CatalogActions.ts b/client/src/app/catalog/CatalogActions.ts new file mode 100644 index 0000000..d3d1746 --- /dev/null +++ b/client/src/app/catalog/CatalogActions.ts @@ -0,0 +1,54 @@ +import { IDataSource } from '@/lib/hooks/useDataSource/DataSource'; + +export type SuccessNotificationResponse = { + message: string; + description?: string; +}; + +export type PermissionResponse = unknown; + +export type IdentityResponse = unknown; + +export type CatalogActionCheckResponse = { + authenticated: boolean; + redirectTo?: string; + logout?: boolean; + error?: Error; +}; + +export type CatalogActionOnErrorResponse = { + redirectTo?: string; + logout?: boolean; + error?: Error; +}; + +export type CatalogActionResponse = { + success: boolean; + redirectTo?: string; + error?: Error; + [key: string]: unknown; + successNotification?: SuccessNotificationResponse; +}; + +export interface ICatalogActions { + listCatalog: ( + dataSource: IDataSource, + pagination: { + pageIndex: number; + pageSize: number; + } = { + pageIndex: INITIAL_PAGE_INDEX, + pageSize: INITIAL_PAGE_SIZE, + }, + quickSearchTerm: string = "", + ): Promise> => { + return dataProvider.getList({ + resource: "invoices", + quickSearchTerm, + pagination, + }); + }; + + + } +} diff --git a/client/src/app/catalog/CatalogContext.tsx b/client/src/app/catalog/CatalogContext.tsx new file mode 100644 index 0000000..6e17e1b --- /dev/null +++ b/client/src/app/catalog/CatalogContext.tsx @@ -0,0 +1,21 @@ +import { usePagination } from "@/lib/hooks"; +import { PropsWithChildren, createContext } from "react"; + +export interface ICatalogContextState {} + +export const CatalogContext = createContext(null); + +export const CatalogProvider = ({ children }: PropsWithChildren) => { + const [pagination, setPagination] = usePagination(); + + return ( + + {children} + + ); +}; diff --git a/client/src/app/catalog/components/CatalogDataTable.tsx b/client/src/app/catalog/components/CatalogDataTable.tsx new file mode 100644 index 0000000..8806553 --- /dev/null +++ b/client/src/app/catalog/components/CatalogDataTable.tsx @@ -0,0 +1,36 @@ +import { DataTable } from "@/components"; +import { useDataTable } from "@/lib/hooks"; +import { IListArticles_Response_DTO, IListResponse_DTO } from "@shared/contexts"; +import { useMemo } from "react"; + +type CatalogTableViewProps = { + data: IListResponse_DTO; +}; + +export const CatalogDataTable = ({ data }: CatalogTableViewProps) => { + const columns = useMemo( + () => [ + { + id: "description" as const, + accessorKey: "description", + size: 400, + enableHiding: false, + enableSorting: false, + enableResizing: false, + }, + ], + [] + ); + + const { table } = useDataTable({ + data: data?.items ?? [], + columns: columns, + pageCount: data?.total_pages ?? -1, + }); + + return ( + <> + + + ); +}; diff --git a/client/src/app/catalog/components/index.ts b/client/src/app/catalog/components/index.ts new file mode 100644 index 0000000..f18e83e --- /dev/null +++ b/client/src/app/catalog/components/index.ts @@ -0,0 +1 @@ +export * from "./CatalogDataTable"; diff --git a/client/src/app/catalog/components/useCatalogTableColumns.tsx b/client/src/app/catalog/components/useCatalogTableColumns.tsx new file mode 100644 index 0000000..d6f9f4f --- /dev/null +++ b/client/src/app/catalog/components/useCatalogTableColumns.tsx @@ -0,0 +1,77 @@ +import { IListArticles_Response_DTO } from "@shared/contexts"; +import { ColumnDef } from "@tanstack/react-table"; + +import { DataTablaRowActionFunction, DataTableRowActions } from "@/components"; +import { Badge } from "@/ui"; +import { useMemo } from "react"; + +export const useCustomerInvoiceDataTableColumns = ( + actions: DataTablaRowActionFunction +): ColumnDef[] => { + const customerColumns: ColumnDef[] = useMemo( + () => [ + /*{ + id: "complete_name", + header: "Nombre", + accessorFn: (row) => ( +
+
+ + + + {acronym(`${row.first_name} ${row.last_name}`)} + + +
+

{`${row.first_name} ${row.last_name}`}

+

{row.job_title}

+
+
+
+ ), + enableSorting: true, + sortingFn: "alphanumeric", + enableHiding: false, + + cell: ({ renderValue }) => ( + + <>{renderValue()} + + ), + },*/ + { + id: "company", + accessorKey: "company_name", + header: "Compañía", + enableSorting: true, + sortingFn: "alphanumeric", + }, + { + id: "state", + accessorKey: "state", + header: "Estado", + cell: ({ renderValue }) => ( + + <>{renderValue()} + + ), + }, + { + id: "phone", + accessorKey: "phone", + header: "Phone", + enableSorting: true, + sortingFn: "alphanumeric", + }, + { + id: "actions", + header: "Acciones", + cell: ({ row }) => { + return ; + }, + }, + ], + [actions] + ); + return customerColumns; +}; diff --git a/client/src/app/catalog/hooks/index.ts b/client/src/app/catalog/hooks/index.ts new file mode 100644 index 0000000..dd06141 --- /dev/null +++ b/client/src/app/catalog/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useCatalogList"; diff --git a/client/src/app/catalog/hooks/useCatalogList.tsx b/client/src/app/catalog/hooks/useCatalogList.tsx new file mode 100644 index 0000000..3ca8e0d --- /dev/null +++ b/client/src/app/catalog/hooks/useCatalogList.tsx @@ -0,0 +1,40 @@ +import { UseListQueryResult, useList } from "@/lib/hooks/useDataSource"; +import { useDataSource } from "@/lib/hooks/useDataSource/useDataSource"; +import { useQueryKey } from "@/lib/hooks/useQueryKey"; +import { IListArticles_Response_DTO, IListResponse_DTO } from "@shared/contexts"; + +export type UseCatalogListParams = { + pagination: { + pageIndex: number; + pageSize: number; + }; + searchTerm?: string; + enabled?: boolean; + queryOptions?: Record; +}; + +export type UseCatalogListResponse = UseListQueryResult< + IListResponse_DTO, + unknown +>; + +export const useCatalogList = (params: UseCatalogListParams): UseCatalogListResponse => { + const dataSource = useDataSource(); + const keys = useQueryKey(); + + const { pagination, searchTerm = undefined, enabled = true, queryOptions } = params; + + return useList({ + queryKey: keys().data().resource("catalog").action("list").params(params).get(), + queryFn: () => { + console.log(pagination); + return dataSource.getList({ + resource: "catalog", + quickSearchTerm: searchTerm, + pagination, + }); + }, + enabled, + queryOptions, + }); +}; diff --git a/client/src/app/catalog/index.ts b/client/src/app/catalog/index.ts index 491ccf0..70c0eed 100644 --- a/client/src/app/catalog/index.ts +++ b/client/src/app/catalog/index.ts @@ -1 +1,2 @@ +export * from "./layout"; export * from "./list"; diff --git a/client/src/app/catalog/layout.tsx b/client/src/app/catalog/layout.tsx new file mode 100644 index 0000000..ef401dc --- /dev/null +++ b/client/src/app/catalog/layout.tsx @@ -0,0 +1,17 @@ +import { Layout, LayoutContent, LayoutHeader } from "@/components"; +import { DataTableProvider } from "@/lib/hooks"; +import { PropsWithChildren } from "react"; +import { CatalogProvider } from "./CatalogContext"; + +export const CatalogLayout = ({ children }: PropsWithChildren) => { + return ( + + + + + {children} + + + + ); +}; diff --git a/client/src/app/catalog/list.tsx b/client/src/app/catalog/list.tsx index 9834599..e2c9b8e 100644 --- a/client/src/app/catalog/list.tsx +++ b/client/src/app/catalog/list.tsx @@ -28,289 +28,383 @@ import { import { File, ListFilter, MoreHorizontal, PlusCircle } from "lucide-react"; -import { Layout, LayoutContent, LayoutHeader } from "@/components"; +import { DataTable, DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components"; + +import { useDataTable, useDataTableContext } from "@/lib/hooks"; +import { useMemo } from "react"; +import { Trans } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { CatalogDataTable } from "./components"; +import { useCatalogList } from "./hooks/useCatalogList"; export const CatalogList = () => { - return ( - - - -
-

Catalog

-
+ const navigate = useNavigate(); - -
- - All - Active - Draft - - Archived - - -
- - - - - - Filter by - - Active - Draft - Archived - - - - -
+ const { pagination } = useDataTableContext(); + console.log("pagination PADRE => ", pagination); + + const { data, isPending, isError, error, refetch } = useCatalogList({ + pagination: { + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + }, + }); + + const columns = useMemo( + () => [ + { + id: "description" as const, + accessorKey: "description", + size: 400, + enableHiding: false, + enableSorting: false, + enableResizing: false, + }, + ], + [] + ); + + const { table } = useDataTable({ + data: data?.items ?? [], + columns: columns, + pageCount: data?.total_pages ?? -1, + }); + + return ; + + if (isError || isPending) { + return <>; + } + + return ( + <> + + {data.items.map((row) => ( +

{row.description}

+ ))} + ; + + ); + + if (isError) { + return ; + } + + if (isPending) { + return ( + + + + + + Manage your products and view their sales performance. + + + + + + ); + } + + if (data?.total_items === 0) { + return ( + navigate("/catalog/add")} + /> + ); + } + + return ( + <> + +
+ + All + Active + Draft + + Archived + + +
+ + + + + + Filter by + + Active + Draft + Archived + + + +
- - - - Products - - Manage your products and view their sales performance. - - - - - - - - Image - - Name - Status - Price - Total Sales - Created at - - Actions - - - - - - - Product image - - Laser Lemonade Machine - - Draft - - $499.99 - 25 - 2023-07-12 10:42 AM - - - - - - - Actions - Edit - Delete - - - - - - - Product image - - Hypernova Headphones - - Active - - $129.99 - 100 - 2023-10-18 03:21 PM - - - - - - - Actions - Edit - Delete - - - - - - - Product image - - AeroGlow Desk Lamp - - Active - - $39.99 - 50 - 2023-11-29 08:15 AM - - - - - - - Actions - Edit - Delete - - - - - - - Product image - - TechTonic Energy Drink - - Draft - - $2.99 - 0 - 2023-12-25 11:59 PM - - - - - - - Actions - Edit - Delete - - - - - - - Product image - - Gamer Gear Pro Controller - - Active - - $59.99 - 75 - 2024-01-01 12:00 AM - - - - - - - Actions - Edit - Delete - - - - - - - Product image - - Luminous VR Headset - - Active - - $199.99 - 30 - 2024-02-14 02:14 PM - - - - - - - Actions - Edit - Delete - - - - - -
-
- -
- Showing 1-10 of 32 products -
-
-
-
- - - +
+ + + + + + + + Manage your products and view their sales performance. + + + + + + + + Image + + Name + Status + Price + Total Sales + Created at + + Actions + + + + + + + Product image + + Laser Lemonade Machine + + Draft + + $499.99 + 25 + 2023-07-12 10:42 AM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + Hypernova Headphones + + Active + + $129.99 + 100 + 2023-10-18 03:21 PM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + AeroGlow Desk Lamp + + Active + + $39.99 + 50 + 2023-11-29 08:15 AM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + TechTonic Energy Drink + + Draft + + $2.99 + 0 + 2023-12-25 11:59 PM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + Gamer Gear Pro Controller + + Active + + $59.99 + 75 + 2024-01-01 12:00 AM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + Luminous VR Headset + + Active + + $199.99 + 30 + 2024-02-14 02:14 PM + + + + + + + Actions + Edit + Delete + + + + + +
+
+ +
+ Showing 1-10 of 32 products +
+
+
+
+
+ ); }; diff --git a/client/src/app/catalog/useCatalogContext.tsx b/client/src/app/catalog/useCatalogContext.tsx new file mode 100644 index 0000000..2ba55ab --- /dev/null +++ b/client/src/app/catalog/useCatalogContext.tsx @@ -0,0 +1,8 @@ +import { useContext } from "react"; +import { CatalogContext } from "./CatalogContext"; + +export const useCatalogContext = () => { + const context = useContext(CatalogContext); + if (context === null) throw new Error("useCatalog must be used within a CatalogProvider"); + return context; +}; diff --git a/client/src/app/dashboard/index.tsx b/client/src/app/dashboard/index.tsx index fc68baa..70abb23 100644 --- a/client/src/app/dashboard/index.tsx +++ b/client/src/app/dashboard/index.tsx @@ -10,6 +10,7 @@ import { } from "lucide-react"; import { Layout, LayoutContent, LayoutHeader } from "@/components"; +import { useGetIdentity } from "@/lib/hooks"; import { Badge, Button, @@ -42,383 +43,396 @@ import { TabsList, TabsTrigger, } from "@/ui"; +import { t } from "i18next"; export const DashboardPage = () => { + const { data, status } = useGetIdentity(); + return ( - -
-
- - - Tus Cotizaciones - - Introducing Our Dynamic Orders Dashboard for Seamless Management and Insightful - Analysis. - + + {status === "success" && ( +
+

{`${t("dashboard.welcome")}, ${ + data?.name + }`}

+
+ )} + +
+
+
+ + + Tus Cotizaciones + + Introducing Our Dynamic Orders Dashboard for Seamless Management and Insightful + Analysis. + + + + + + + + + This Week + $1,329 + + +
+25% from last week
+
+ + + +
+ + + This Month + $5,329 + + +
+10% from last month
+
+ + + +
+
+ +
+ + Week + Month + Year + +
+ + + + + + Filter by + + Fulfilled + Declined + Refunded + + + +
+
+ + + + Orders + Recent orders from your store. + + + + + + Customer + Type + Status + Date + Amount + + + + + +
Liam Johnson
+
+ liam@example.com +
+
+ Sale + + + Fulfilled + + + 2023-06-23 + $250.00 +
+ + +
Olivia Smith
+
+ olivia@example.com +
+
+ Refund + + + Declined + + + 2023-06-24 + $150.00 +
+ + +
Noah Williams
+
+ noah@example.com +
+
+ Subscription + + + Fulfilled + + + 2023-06-25 + $350.00 +
+ + +
Emma Brown
+
+ emma@example.com +
+
+ Sale + + + Fulfilled + + + 2023-06-26 + $450.00 +
+ + +
Liam Johnson
+
+ liam@example.com +
+
+ Sale + + + Fulfilled + + + 2023-06-23 + $250.00 +
+ + +
Liam Johnson
+
+ liam@example.com +
+
+ Sale + + + Fulfilled + + + 2023-06-23 + $250.00 +
+ + +
Olivia Smith
+
+ olivia@example.com +
+
+ Refund + + + Declined + + + 2023-06-24 + $150.00 +
+ + +
Emma Brown
+
+ emma@example.com +
+
+ Sale + + + Fulfilled + + + 2023-06-26 + $450.00 +
+
+
+
+
+
+
+
+
+ + +
+ + Order Oe31b70H + + + Date: November 23, 2023 +
+
+ + + + + + + Edit + Export + + Trash + + +
- - - -
- - - This Week - $1,329 - - -
+25% from last week
+ +
+
Order Details
+
    +
  • + + Glimmer Lamps x 2 + + $250.00 +
  • +
  • + + Aqua Filters x 1 + + $49.00 +
  • +
+ +
    +
  • + Subtotal + $299.00 +
  • +
  • + Shipping + $5.00 +
  • +
  • + Tax + $25.00 +
  • +
  • + Total + $329.00 +
  • +
+
+ +
+
+
Shipping Information
+
+ Liam Johnson + 1234 Main St. + Anytown, CA 12345 +
+
+
+
Billing Information
+
Same as shipping address
+
+
+ +
+
Customer Information
+
+
+
Customer
+
Liam Johnson
+
+
+
Email
+
+ liam@acme.com +
+
+
+
Phone
+
+ +1 234 567 890 +
+
+
+
+ +
+
Payment Information
+
+
+
+ + Visa +
+
**** **** **** 4532
+
+
+
- - - -
- - - This Month - $5,329 - - -
+10% from last month
-
- - + +
+ Updated +
+ + + + + + + + + +
- -
- - Week - Month - Year - -
- - - - - - Filter by - - Fulfilled - Declined - Refunded - - - -
-
- - - - Orders - Recent orders from your store. - - - - - - Customer - Type - Status - Date - Amount - - - - - -
Liam Johnson
-
- liam@example.com -
-
- Sale - - - Fulfilled - - - 2023-06-23 - $250.00 -
- - -
Olivia Smith
-
- olivia@example.com -
-
- Refund - - - Declined - - - 2023-06-24 - $150.00 -
- - -
Noah Williams
-
- noah@example.com -
-
- Subscription - - - Fulfilled - - - 2023-06-25 - $350.00 -
- - -
Emma Brown
-
- emma@example.com -
-
- Sale - - - Fulfilled - - - 2023-06-26 - $450.00 -
- - -
Liam Johnson
-
- liam@example.com -
-
- Sale - - - Fulfilled - - - 2023-06-23 - $250.00 -
- - -
Liam Johnson
-
- liam@example.com -
-
- Sale - - - Fulfilled - - - 2023-06-23 - $250.00 -
- - -
Olivia Smith
-
- olivia@example.com -
-
- Refund - - - Declined - - - 2023-06-24 - $150.00 -
- - -
Emma Brown
-
- emma@example.com -
-
- Sale - - - Fulfilled - - - 2023-06-26 - $450.00 -
-
-
-
-
-
-
-
-
- - -
- - Order Oe31b70H - - - Date: November 23, 2023 -
-
- - - - - - - Edit - Export - - Trash - - -
-
- -
-
Order Details
-
    -
  • - - Glimmer Lamps x 2 - - $250.00 -
  • -
  • - - Aqua Filters x 1 - - $49.00 -
  • -
- -
    -
  • - Subtotal - $299.00 -
  • -
  • - Shipping - $5.00 -
  • -
  • - Tax - $25.00 -
  • -
  • - Total - $329.00 -
  • -
-
- -
-
-
Shipping Information
-
- Liam Johnson - 1234 Main St. - Anytown, CA 12345 -
-
-
-
Billing Information
-
Same as shipping address
-
-
- -
-
Customer Information
-
-
-
Customer
-
Liam Johnson
-
-
-
Email
-
- liam@acme.com -
-
-
-
Phone
-
- +1 234 567 890 -
-
-
-
- -
-
Payment Information
-
-
-
- - Visa -
-
**** **** **** 4532
-
-
-
-
- -
- Updated -
- - - - - - - - - - -
-
diff --git a/client/src/app/dealers/index.ts b/client/src/app/dealers/index.ts index 491ccf0..70c0eed 100644 --- a/client/src/app/dealers/index.ts +++ b/client/src/app/dealers/index.ts @@ -1 +1,2 @@ +export * from "./layout"; export * from "./list"; diff --git a/client/src/app/dealers/layout.tsx b/client/src/app/dealers/layout.tsx new file mode 100644 index 0000000..169331e --- /dev/null +++ b/client/src/app/dealers/layout.tsx @@ -0,0 +1,11 @@ +import { Layout, LayoutContent, LayoutHeader } from "@/components"; +import { PropsWithChildren } from "react"; + +export const DealerLayout = ({ children }: PropsWithChildren) => { + return ( + + + {children} + + ); +}; diff --git a/client/src/app/dealers/list.tsx b/client/src/app/dealers/list.tsx index 067efcd..6e6c67f 100644 --- a/client/src/app/dealers/list.tsx +++ b/client/src/app/dealers/list.tsx @@ -1,14 +1,311 @@ -import { Layout, LayoutContent, LayoutHeader } from "@/components"; +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/ui"; + +import { File, ListFilter, MoreHorizontal, PlusCircle } from "lucide-react"; + +import { Trans } from "react-i18next"; export const DealersList = () => { return ( - - - + <> +
-

Dealers

+ + All + Active + Draft + + Archived + + +
+ + + + + + Filter by + + Active + Draft + Archived + + + + +
-
-
+ + + + + + + + Manage your products and view their sales performance. + + + + + + + + Image + + Name + Status + Price + Total Sales + Created at + + Actions + + + + + + + Product image + + Laser Lemonade Machine + + Draft + + $499.99 + 25 + 2023-07-12 10:42 AM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + Hypernova Headphones + + Active + + $129.99 + 100 + 2023-10-18 03:21 PM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + AeroGlow Desk Lamp + + Active + + $39.99 + 50 + 2023-11-29 08:15 AM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + TechTonic Energy Drink + + Draft + + $2.99 + 0 + 2023-12-25 11:59 PM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + Gamer Gear Pro Controller + + Active + + $59.99 + 75 + 2024-01-01 12:00 AM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + Luminous VR Headset + + Active + + $199.99 + 30 + 2024-02-14 02:14 PM + + + + + + + Actions + Edit + Delete + + + + + +
+
+ +
+ Showing 1-10 of 32 products +
+
+
+
+ + ); }; diff --git a/client/src/components/ButtonGroup/ButtonGroup.tsx b/client/src/components/ButtonGroup/ButtonGroup.tsx new file mode 100644 index 0000000..91fcfc2 --- /dev/null +++ b/client/src/components/ButtonGroup/ButtonGroup.tsx @@ -0,0 +1,14 @@ +import { cn } from "@/lib/utils"; +import React from "react"; + +export const ButtonGroup = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +ButtonGroup.displayName = "ButtonGroup"; diff --git a/client/src/components/ButtonGroup/index.ts b/client/src/components/ButtonGroup/index.ts new file mode 100644 index 0000000..d22eaf4 --- /dev/null +++ b/client/src/components/ButtonGroup/index.ts @@ -0,0 +1 @@ +export * from "./ButtonGroup"; diff --git a/client/src/components/CustomButtons/BackHistoryButton.tsx b/client/src/components/CustomButtons/BackHistoryButton.tsx new file mode 100644 index 0000000..650637c --- /dev/null +++ b/client/src/components/CustomButtons/BackHistoryButton.tsx @@ -0,0 +1,31 @@ +import { Button, ButtonProps } from "@/ui"; +import { ChevronLeft } from "lucide-react"; +import { To, useNavigate } from "react-router-dom"; + +export interface BackHistoryButtonProps extends ButtonProps { + label?: string; + url?: To; +} + +export const BackHistoryButton = ({ + label = "Volver atrás", + size, + url = undefined, + ...props +}: BackHistoryButtonProps): JSX.Element => { + const navigate = useNavigate(); + + return ( + + ); +}; diff --git a/client/src/components/CustomButtons/index.ts b/client/src/components/CustomButtons/index.ts new file mode 100644 index 0000000..d5c95b0 --- /dev/null +++ b/client/src/components/CustomButtons/index.ts @@ -0,0 +1 @@ +export * from "./BackHistoryButton"; diff --git a/client/src/components/DataTable/DataTable.tsx b/client/src/components/DataTable/DataTable.tsx new file mode 100644 index 0000000..6128cc3 --- /dev/null +++ b/client/src/components/DataTable/DataTable.tsx @@ -0,0 +1,97 @@ +import { ColumnDef, Table as ReactTable, flexRender } from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/ui/table"; +import { ReactNode } from "react"; + +import { Card, CardContent, CardFooter, CardHeader } from "@/ui"; +import { DataTableColumnHeader } from "./DataTableColumnHeader"; +import { DataTablePagination, DataTablePaginationProps } from "./DataTablePagination"; +import { DataTableToolbar } from "./DataTableToolbar"; + +export type DataTableColumnProps = ColumnDef; + +export type DataTablePaginationOptionsProps = Pick< + DataTablePaginationProps, + "visible" +>; + +export type DataTableProps = { + table: ReactTable; + caption?: ReactNode; + className?: string; + + paginationOptions?: DataTablePaginationOptionsProps; +}; + +export function DataTable({ table, caption, paginationOptions }: DataTableProps) { + return ( + <> + + + + + + + + {typeof caption !== "undefined" && {caption}} + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No hay datos para mostrar + + + )} + +
+
+ + + +
+ + ); +} diff --git a/client/src/components/DataTable/DataTableColumnHeader.tsx b/client/src/components/DataTable/DataTableColumnHeader.tsx new file mode 100644 index 0000000..5b1d1d8 --- /dev/null +++ b/client/src/components/DataTable/DataTableColumnHeader.tsx @@ -0,0 +1,123 @@ +import { Header, Table, flexRender } from "@tanstack/react-table"; + +import { cn } from "@/lib/utils"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, + Separator, +} from "@/ui"; +import { + ArrowDownIcon, + ArrowDownUpIcon, + ArrowUpIcon, + EyeOffIcon, +} from "lucide-react"; + +interface DataTableColumnHeaderProps + extends React.HTMLAttributes { + table: Table; + header: Header; +} + +export function DataTableColumnHeader({ + table, + header, + className, +}: DataTableColumnHeaderProps) { + if (!header.column.getCanSort()) { + return ( + <> +
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
+ {header.column.getCanResize() && ( + header.column.resetSize(), + onMouseDown: header.getResizeHandler(), + onTouchStart: header.getResizeHandler(), + style: { + transform: + table.options.columnResizeMode === "onEnd" && + header.column.getIsResizing() + ? `translateX(${ + (table.options.columnResizeDirection === "rtl" + ? -1 + : 1) * + (table.getState().columnSizingInfo.deltaOffset ?? 0) + }px)` + : "", + }, + }} + /> + )} + + ); + } + + return ( +
+ + + + + + header.column.toggleSorting(false)}> + + Ascendente + + header.column.toggleSorting(true)}> + + Descendente + + + {header.column.getCanHide() && ( + header.column.toggleVisibility(false)} + > + + Ocultar + + )} + + +
+ ); +} diff --git a/client/src/components/DataTable/DataTableFacetedFilter.tsx b/client/src/components/DataTable/DataTableFacetedFilter.tsx new file mode 100644 index 0000000..9fedd70 --- /dev/null +++ b/client/src/components/DataTable/DataTableFacetedFilter.tsx @@ -0,0 +1,149 @@ +import { Column } from "@tanstack/react-table" +import * as React from "react" + +import { cn } from "@/lib/utils" +import { + Badge, + Button, + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/ui" + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/ui" + +import { Separator } from "@/ui" +import { CheckIcon, PlusCircleIcon } from 'lucide-react' + +interface DataTableFacetedFilterProps { + column?: Column + title?: string + options: { + label: string + value: string + icon?: React.ComponentType<{ className?: string }> + }[] +} + +export function DataTableFacetedFilter({ + column, + title, + options, +}: DataTableFacetedFilterProps) { + const facets = column?.getFacetedUniqueValues() + const selectedValues = new Set(column?.getFilterValue() as string[]) + + return ( + + + + + + + + + No results found. + + {options.map((option) => { + const isSelected = selectedValues.has(option.value) + return ( + { + if (isSelected) { + selectedValues.delete(option.value) + } else { + selectedValues.add(option.value) + } + const filterValues = Array.from(selectedValues) + column?.setFilterValue( + filterValues.length ? filterValues : undefined + ) + }} + > +
+ +
+ {option.icon && ( + + )} + {option.label} + {facets?.get(option.value) && ( + + {facets.get(option.value)} + + )} +
+ ) + })} +
+ {selectedValues.size > 0 && ( + <> + + + column?.setFilterValue(undefined)} + className="justify-center text-center" + > + Clear filters + + + + )} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/client/src/components/DataTable/DataTablePagination.tsx b/client/src/components/DataTable/DataTablePagination.tsx new file mode 100644 index 0000000..1066d27 --- /dev/null +++ b/client/src/components/DataTable/DataTablePagination.tsx @@ -0,0 +1,102 @@ +import { DEFAULT_PAGE_SIZES, INITIAL_PAGE_INDEX } from "@/lib/hooks"; +import { cn } from "@/lib/utils"; +import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/ui"; +import { Table } from "@tanstack/react-table"; +import { + ChevronLeftIcon, + ChevronRightIcon, + ChevronsLeftIcon, + ChevronsRightIcon, +} from "lucide-react"; +import { useMemo } from "react"; + +export type DataTablePaginationProps = { + table: Table; + className?: string; + visible?: boolean | "auto"; +}; + +export function DataTablePagination({ + table, + className, + visible = "auto", +}: DataTablePaginationProps) { + const isVisible = useMemo(() => visible === true, [visible]); + const isAuto = useMemo(() => visible === "auto", [visible]); + + if (!isVisible || (isAuto && table.getPageCount() < 1)) { + return null; + } + + return ( +
+
+ {table.getFilteredSelectedRowModel().rows.length} de{" "} + {table.getFilteredRowModel().rows.length} filas(s) seleccionadas. +
+
+
+

Filas por página

+ +
+
+ Página {table.getState().pagination.pageIndex + 1} de {table.getPageCount()} +
+
+ + + + +
+
+
+ ); +} diff --git a/client/src/components/DataTable/DataTableRowActions.tsx b/client/src/components/DataTable/DataTableRowActions.tsx new file mode 100644 index 0000000..9da8b86 --- /dev/null +++ b/client/src/components/DataTable/DataTableRowActions.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/ui"; +import { CellContext } from "@tanstack/react-table"; +import { MoreHorizontalIcon } from "lucide-react"; + +type DataTableRowActionContext = CellContext; + +export type DataTablaRowActionFunction = ( + props: DataTableRowActionContext, +) => DataTableRowActionDefinition[]; + +export type DataTableRowActionDefinition = { + label: string | "-"; + shortcut?: string; + onClick?: ( + props: DataTableRowActionContext, + e: React.BaseSyntheticEvent, + ) => void; +}; + +export type DataTableRowActionsProps = { + props: DataTableRowActionContext; + actions?: DataTablaRowActionFunction; +}; + +export function DataTableRowActions({ + actions, + ...props +}: DataTableRowActionsProps) { + return ( + + + + + + + Acciones + {actions && + actions(props).map((action, index) => + action.label === "-" ? ( + + ) : ( + + action.onClick ? action.onClick(props, event) : null + } + > + {action.label} + {action.shortcut} + + ), + )} + + + ); +} diff --git a/client/src/components/DataTable/DataTableRowDragHandleCell.tsx b/client/src/components/DataTable/DataTableRowDragHandleCell.tsx new file mode 100644 index 0000000..ed4409d --- /dev/null +++ b/client/src/components/DataTable/DataTableRowDragHandleCell.tsx @@ -0,0 +1,30 @@ +import { cn } from "@/lib/utils"; +import { Button } from "@/ui"; +import { useSortable } from "@dnd-kit/sortable"; +import { GripVerticalIcon } from "lucide-react"; + +export const DataTableRowDragHandleCell = ({ rowId }: { rowId: string }) => { + const { attributes, listeners, isDragging } = useSortable({ + id: rowId, + }); + + return ( + + ); +}; diff --git a/client/src/components/DataTable/DataTableSelectionColumn.tsx b/client/src/components/DataTable/DataTableSelectionColumn.tsx new file mode 100644 index 0000000..6a63935 --- /dev/null +++ b/client/src/components/DataTable/DataTableSelectionColumn.tsx @@ -0,0 +1,36 @@ +import { Checkbox } from "@/ui"; +import { DataTableColumnProps } from "./DataTable"; + +export function getDataTableSelectionColumn< + TData, + TError, +>(): DataTableColumnProps { + return { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Seleccionar todo" + className="translate-y-[2px]" + /> + ), + cell: ({ row, table }) => ( + { + row.toggleSelected(!!value); + }} + aria-label="Seleccionar file" + className="translate-y-[2px]" + /> + ), + enableSorting: false, + enableHiding: false, + }; +} diff --git a/client/src/components/DataTable/DataTableSkeleton.tsx b/client/src/components/DataTable/DataTableSkeleton.tsx new file mode 100644 index 0000000..30ff5b6 --- /dev/null +++ b/client/src/components/DataTable/DataTableSkeleton.tsx @@ -0,0 +1,149 @@ +import { + Skeleton, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/ui"; + +interface DataTableSkeletonProps { + /** + * The number of columns in the table. + * @type number + */ + columnCount: number; + + /** + * The number of rows in the table. + * @default 10 + * @type number | undefined + */ + rowCount?: number; + + /** + * The number of searchable columns in the table. + * @default 0 + * @type number | undefined + */ + searchableColumnCount?: number; + + /** + * The number of filterable columns in the table. + * @default 0 + * @type number | undefined + */ + filterableColumnCount?: number; + + /** + * Flag to show the table view options. + * @default undefined + * @type boolean | undefined + */ + showViewOptions?: boolean; + + /** + * The width of each cell in the table. + * The length of the array should be equal to the columnCount. + * Any valid CSS width value is accepted. + * @default ["auto"] + * @type string[] | undefined + */ + cellWidths?: string[]; + + /** + * Flag to prevent the table from shrinking to fit the content. + * @default false + * @type boolean | undefined + */ + shrinkZero?: boolean; +} + +export function DataTableSkeleton({ + columnCount, + rowCount = 10, + searchableColumnCount = 0, + filterableColumnCount = 0, + showViewOptions = true, + cellWidths = ["auto"], + shrinkZero = false, +}: DataTableSkeletonProps) { + return ( +
+
+
+ {searchableColumnCount > 0 + ? Array.from({ length: searchableColumnCount }).map((_, i) => ( + + )) + : null} + {filterableColumnCount > 0 + ? Array.from({ length: filterableColumnCount }).map((_, i) => ( + + )) + : null} +
+ {showViewOptions ? ( + + ) : null} +
+
+ + + {Array.from({ length: 1 }).map((_, i) => ( + + {Array.from({ length: columnCount }).map((_, j) => ( + + + + ))} + + ))} + + + {Array.from({ length: rowCount }).map((_, i) => ( + + {Array.from({ length: columnCount }).map((_, j) => ( + + + + ))} + + ))} + +
+
+
+ +
+
+ + +
+
+ +
+
+ + + + +
+
+
+
+ ); +} diff --git a/client/src/components/DataTable/DataTableToolbar/DataTableColumnOptions.tsx b/client/src/components/DataTable/DataTableToolbar/DataTableColumnOptions.tsx new file mode 100644 index 0000000..0a1d104 --- /dev/null +++ b/client/src/components/DataTable/DataTableToolbar/DataTableColumnOptions.tsx @@ -0,0 +1,57 @@ +import { Table } from "@tanstack/react-table"; + +import { + Button, + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/ui"; +import { Settings2Icon } from "lucide-react"; + +interface DataTableColumnOptionsProps { + table: Table; +} + +export function DataTableColumnOptions({ + table, +}: DataTableColumnOptionsProps) { + return ( + + + + + + Columnas + + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== "undefined" && column.getCanHide() + ) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + > + <>{column.columnDef.header} + + ); + })} + + + ); +} diff --git a/client/src/components/DataTable/DataTableToolbar/DataTableToolbar.tsx b/client/src/components/DataTable/DataTableToolbar/DataTableToolbar.tsx new file mode 100644 index 0000000..28ee7d6 --- /dev/null +++ b/client/src/components/DataTable/DataTableToolbar/DataTableToolbar.tsx @@ -0,0 +1,138 @@ +import { Table } from "@tanstack/react-table"; + +//import { priorities, statuses } from './Data' +import { DataTableFilterField } from "@/lib/types"; +import { cn } from "@/lib/utils"; +import { Button, Input } from "@/ui"; +import { CrossIcon } from "lucide-react"; +import { useMemo } from "react"; +import { DataTableFacetedFilter } from "../DataTableFacetedFilter"; +import { DataTableColumnOptions } from "./DataTableColumnOptions"; + +interface DataTableToolbarProps + extends React.HTMLAttributes { + table: Table; + filterFields?: DataTableFilterField[]; +} + +export function DataTableToolbar({ + table, + filterFields = [], + children, + className, + ...props +}: DataTableToolbarProps) { + const isFiltered = table.getState().columnFilters.length > 0; + + // Memoize computation of searchableColumns and filterableColumns + const { searchableColumns, filterableColumns } = useMemo(() => { + return { + searchableColumns: filterFields.filter((field) => !field.options), + filterableColumns: filterFields.filter((field) => field.options), + }; + }, [filterFields]); + + return ( +
+
+ {searchableColumns.length > 0 && + searchableColumns.map( + (column) => + table.getColumn(column.value ? String(column.value) : "") && ( + + table + .getColumn(String(column.value)) + ?.setFilterValue(event.target.value) + } + className="w-40 h-8 lg:w-64" + /> + ), + )} + {filterableColumns.length > 0 && + filterableColumns.map( + (column) => + table.getColumn(column.value ? String(column.value) : "") && ( + + ), + )} + {isFiltered && ( + + )} +
+
+ {children} + {table.options.enableHiding && } +
+
+ ); + + /* + return ( +
+
+ + table.getColumn("customer")?.setFilterValue(event.target.value) + } + className="h-8 w-[150px] lg:w-[250px]" + /> + {table.getColumn("status") && ( + + )} + {table.getColumn("priority") && ( + + )} + {isFiltered && ( + + )} +
+ +
+ ) + */ +} diff --git a/client/src/components/DataTable/DataTableToolbar/index.ts b/client/src/components/DataTable/DataTableToolbar/index.ts new file mode 100644 index 0000000..e878c23 --- /dev/null +++ b/client/src/components/DataTable/DataTableToolbar/index.ts @@ -0,0 +1 @@ +export * from "./DataTableToolbar"; diff --git a/client/src/components/DataTable/index.ts b/client/src/components/DataTable/index.ts new file mode 100644 index 0000000..9d0ca90 --- /dev/null +++ b/client/src/components/DataTable/index.ts @@ -0,0 +1,6 @@ +export * from "./DataTable"; +export * from "./DataTableColumnHeader"; +export * from "./DataTableRowActions"; +export * from "./DataTableRowDragHandleCell"; +export * from "./DataTableSelectionColumn"; +export * from "./DataTableSkeleton"; diff --git a/client/src/components/EmptyState/SimpleEmptyState.tsx b/client/src/components/EmptyState/SimpleEmptyState.tsx new file mode 100644 index 0000000..8b3b440 --- /dev/null +++ b/client/src/components/EmptyState/SimpleEmptyState.tsx @@ -0,0 +1,42 @@ +import { Button } from "@/ui"; +import { PlusIcon } from "lucide-react"; + +export const SimpleEmptyState = ({ + title = "Esto está muy vacío", + subtitle = "Empieza dando de alta un item", + buttonText = "Nuevo item", + onButtonClick = () => {}, + actions = undefined, +}) => { + return ( +
+ +

{title}

+

{subtitle}

+ +
+ {actions && <>{actions}} + {!actions && ( + + )} +
+
+ ); +}; diff --git a/client/src/components/EmptyState/index.ts b/client/src/components/EmptyState/index.ts new file mode 100644 index 0000000..910fd71 --- /dev/null +++ b/client/src/components/EmptyState/index.ts @@ -0,0 +1 @@ +export * from "./SimpleEmptyState"; diff --git a/client/src/components/ErrorOverlay/ErrorOverlay.module.css b/client/src/components/ErrorOverlay/ErrorOverlay.module.css new file mode 100644 index 0000000..672dbf3 --- /dev/null +++ b/client/src/components/ErrorOverlay/ErrorOverlay.module.css @@ -0,0 +1,20 @@ +@tailwind components; + +@layer components { + .ErrorOverlay { + @apply fixed top-0 bottom-0 left-0 right-0 z-50 w-full h-screen overflow-hidden; + @apply flex justify-center bg-slate-700 opacity-75; + } + + .ErrorOverlay__container { + @apply flex flex-col items-center justify-center max-w-xs; + } + + .ErrorOverlay__title { + @apply mt-6 text-xl font-semibold text-center text-white; + } + + .ErrorOverlay__subtitle { + @apply text-center text-white; + } +} diff --git a/client/src/components/ErrorOverlay/ErrorOverlay.tsx b/client/src/components/ErrorOverlay/ErrorOverlay.tsx new file mode 100644 index 0000000..d0caac7 --- /dev/null +++ b/client/src/components/ErrorOverlay/ErrorOverlay.tsx @@ -0,0 +1,53 @@ +import { Button } from "@/ui"; +import { HomeIcon } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { BackHistoryButton } from "../CustomButtons"; + +interface ErrorOverlayProps { + title?: string; + subtitle?: string; + description?: string; + + errorMessage?: string; + //errorStatusCode?: number | string | undefined; +} + +/*const _DrawByStatusCode = { + 0: UndrawAutumn, + 404: UndrawNoData, //UndrawEmpty, +};*/ + +export const ErrorOverlay = ({ + title = "Se ha producido un error", + subtitle = undefined, + description = undefined, + errorMessage = undefined, +}: //errorStatusCode = undefined, +ErrorOverlayProps): JSX.Element => { + const navigate = useNavigate(); + /*const ErrorIllustration = () => + String(errorStatusCode).length + ? _DrawByStatusCode[String(errorStatusCode)] + : _DrawByStatusCode['0'];*/ + + return ( +
+
+

{title}

+

+ {subtitle || errorMessage} +
+ {description} +

+
+ + +
+
+
+ ); +}; + +ErrorOverlay.displayName = "ErrorOverlay"; diff --git a/client/src/components/ErrorOverlay/index.ts b/client/src/components/ErrorOverlay/index.ts new file mode 100644 index 0000000..217259b --- /dev/null +++ b/client/src/components/ErrorOverlay/index.ts @@ -0,0 +1 @@ +export * from './ErrorOverlay'; diff --git a/client/src/components/SorteableDataTable/AddNewRowButton.tsx b/client/src/components/SorteableDataTable/AddNewRowButton.tsx new file mode 100644 index 0000000..c9b7363 --- /dev/null +++ b/client/src/components/SorteableDataTable/AddNewRowButton.tsx @@ -0,0 +1,30 @@ +import { cn } from "@/lib/utils"; +import { Button, ButtonProps } from "@/ui"; +import { PlusCircleIcon } from "lucide-react"; + +export interface AddNewRowButtonProps extends ButtonProps { + label?: string; + className?: string; +} + +export const AddNewRowButton = ({ + label = "Añade nueva fila", + className, + ...props +}: AddNewRowButtonProps): JSX.Element => ( + +); + +AddNewRowButton.displayName = "AddNewRowButton"; diff --git a/client/src/components/SorteableDataTable/SortableDataTable.tsx b/client/src/components/SorteableDataTable/SortableDataTable.tsx new file mode 100644 index 0000000..132b240 --- /dev/null +++ b/client/src/components/SorteableDataTable/SortableDataTable.tsx @@ -0,0 +1,463 @@ +import { DataTableColumnHeader } from "@/components"; +import { Badge } from "@/ui"; +import { + Table, + TableBody, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +} from "@/ui/table"; +import { + DndContext, + DragEndEvent, + DragOverlay, + DragStartEvent, + DropAnimation, + KeyboardSensor, + MeasuringStrategy, + MouseSensor, + PointerSensor, + TouchSensor, + UniqueIdentifier, + closestCenter, + defaultDropAnimation, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { + ColumnDef, + Row, + RowData, + RowSelectionState, + VisibilityState, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { useCallback, useMemo, useState } from "react"; +import { createPortal } from "react-dom"; +import { FieldValues, UseFieldArrayReturn } from "react-hook-form"; +import { AddNewRowButton } from "./AddNewRowButton"; +import { SortableDataTableToolbar } from "./SortableDataTableToolbar"; +import { SortableTableRow } from "./SortableTableRow"; + +declare module "@tanstack/react-table" { + interface TableMeta { + insertItem: (rowIndex: number, data: TData) => void; + appendItem: (data: TData) => void; + duplicateItems: (rowIndex?: number) => void; + deleteItems: (rowIndex?: number | number[]) => void; + updateItem: (rowIndex: number, rowData: TData, fieldName: string, value: unknown) => void; + } +} + +export interface SortableProps { + id: UniqueIdentifier; +} + +export type SortableDataTableProps = { + columns: ColumnDef[]; + data: Record<"id", string>[]; + actions: Omit, "fields">; +}; + +const measuringConfig = { + droppable: { + strategy: MeasuringStrategy.Always, + }, +}; + +const dropAnimationConfig: DropAnimation = { + keyframes({ transform }) { + return [ + { opacity: 1, transform: CSS.Transform.toString(transform.initial) }, + { + opacity: 0, + transform: CSS.Transform.toString({ + ...transform.final, + x: transform.final.x + 5, + y: transform.final.y + 5, + }), + }, + ]; + }, + easing: "ease-out", + sideEffects({ active }) { + active.node.animate([{ opacity: 0 }, { opacity: 1 }], { + duration: defaultDropAnimation.duration, + easing: defaultDropAnimation.easing, + }); + }, +}; + +/*const defaultColumn: Partial> = { + cell: ({ table, row: { index, original }, column, getValue }) => { + const initialValue = getValue(); + + // We need to keep and update the state of the cell normally + // eslint-disable-next-line react-hooks/rules-of-hooks + const [value, setValue] = useState(initialValue); + + // If the initialValue is changed external, sync it up with our state + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + setValue(initialValue); + }, [initialValue]); + + return ( + setValue(e.target.value)} + onBlur={() => { + console.log(column.id, value); + table.options.meta?.updateItem(index, original, column.id, value); + }} + /> + ); + }, +};*/ + +export function SortableDataTable({ columns, data, actions }: SortableDataTableProps) { + const [rowSelection, setRowSelection] = useState({}); + const [activeId, setActiveId] = useState(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [columnVisibility, setColumnVisibility] = useState({}); + const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]); + + const table = useReactTable({ + data, + columns, + enableColumnResizing: false, + columnResizeMode: "onChange", + //defaultColumn, + state: { + rowSelection, + columnVisibility, + }, + enableRowSelection: true, + enableMultiRowSelection: true, + enableSorting: false, + enableHiding: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + getRowId: (originalRow: unknown) => originalRow?.id, + debugHeaders: true, + debugColumns: true, + meta: { + insertItem: (rowIndex: number, data: object = {}) => { + actions.insert(rowIndex, data, { shouldFocus: true }); + }, + appendItem: (data: object = {}) => { + actions.append(data, { shouldFocus: true }); + }, + duplicateItems: (rowIndex?: number) => { + if (rowIndex != undefined) { + const originalData = table.getRowModel().rows[rowIndex].original; + actions.insert(rowIndex + 1, originalData, { shouldFocus: true }); + } else if (table.getSelectedRowModel().rows.length) { + const lastIndex = + table.getSelectedRowModel().rows[table.getSelectedRowModel().rows.length - 1].index; + const data = table + .getSelectedRowModel() + .rows.map((row) => ({ ...row.original, id: undefined })); + + if (table.getRowModel().rows.length < lastIndex + 1) { + actions.append(data); + } else { + actions.insert(lastIndex + 1, data, { shouldFocus: true }); + } + table.resetRowSelection(); + } + }, + deleteItems: (rowIndex?: number | number[]) => { + if (rowIndex != undefined) { + actions.remove(rowIndex); + } else if (table.getSelectedRowModel().rows.length > 0) { + let start = table.getSelectedRowModel().rows.length - 1; + for (; start >= 0; start--) { + const oldIndex = sorteableRowIds.indexOf( + String(table.getSelectedRowModel().rows[start].id) + ); + actions.remove(oldIndex); + sorteableRowIds.splice(oldIndex, 1); + } + + /*table.getSelectedRowModel().rows.forEach((row) => { + });*/ + table.resetRowSelection(); + } else { + actions.remove(); + } + }, + updateItem: (rowIndex: number, rowData: unknown, fieldName: string, value: unknown) => { + // Skip page index reset until after next rerender + // skipAutoResetPageIndex(); + console.log({ + rowIndex, + rowData, + fieldName, + value, + }); + + actions.update(rowIndex, { ...rowData, [`${fieldName}`]: value }); + }, + }, + }); + + const sensors = useSensors( + useSensor(MouseSensor, {}), + useSensor(TouchSensor, {}), + useSensor(KeyboardSensor, {}), + useSensor(PointerSensor, {}) + ); + + function handleDragEnd(event: DragEndEvent) { + let activeId = event.active.id; + let overId = event.over?.id; + + if (overId !== undefined && activeId !== overId) { + let newIndex = sorteableRowIds.indexOf(String(overId)); + + if (table.getSelectedRowModel().rows.length > 1) { + table.getSelectedRowModel().rows.forEach((row, index) => { + const oldIndex = sorteableRowIds.indexOf(String(row.id)); + + if (index > 0) { + activeId = row.id; + newIndex = sorteableRowIds.indexOf(String(overId)); + if (newIndex < oldIndex) { + newIndex = newIndex + 1; + } + } + + actions.move(oldIndex, newIndex); + sorteableRowIds.splice(newIndex, 0, sorteableRowIds.splice(oldIndex, 1)[0]); + + overId = row.id; + }); + } else { + const oldIndex = sorteableRowIds.indexOf(String(activeId)); + actions.move(oldIndex, newIndex); + } + } + + setActiveId(null); + } + + function handleDragStart({ active }: DragStartEvent) { + if (!table.getSelectedRowModel().rowsById[active.id]) { + table.resetRowSelection(); + } + + setActiveId(active.id); + } + + function handleDragCancel() { + setActiveId(null); + } + + const hadleNewItem = useCallback(() => { + actions.append([ + { + description: "a", + }, + { + description: "b", + }, + { + description: "c", + }, + { + description: "d", + }, + ]); + }, [actions]); + + function filterItems(items: string[] | Row[]) { + if (!activeId) { + return items; + } + + return items.filter((idOrRow) => { + const id = typeof idOrRow === "string" ? idOrRow : idOrRow.id; + return id === activeId || !table.getSelectedRowModel().rowsById[id]; + }); + } + + return ( + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : ( + + )} + + ); + })} + + ))} + + + + {filterItems(table.getRowModel().rows).map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + + + + + + + + +
+ + {createPortal( + + {activeId && ( +
+ {table.getSelectedRowModel().rows.length ? ( + + {table.getSelectedRowModel().rows.length} + + ) : null} +
+ + + {table.getRowModel().rows.map( + (row) => + row.id === activeId && ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ) + )} + +
+
+ + {table.getSelectedRowModel().rows.length > 1 && ( +
+ + + {table.getRowModel().rows.map( + (row) => + row.id === activeId && ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ) + )} + +
+
+ )} + + {table.getSelectedRowModel().rows.length > 2 && ( +
+ + + {table.getRowModel().rows.map( + (row) => + row.id === activeId && ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ) + )} + +
+
+ )} + + {table.getSelectedRowModel().rows.length > 3 && ( +
+ + + {table.getRowModel().rows.map( + (row) => + row.id === activeId && ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ) + )} + +
+
+ )} +
+ )} +
, + document.body + )} +
+ ); +} diff --git a/client/src/components/SorteableDataTable/SortableDataTableToolbar.tsx b/client/src/components/SorteableDataTable/SortableDataTableToolbar.tsx new file mode 100644 index 0000000..785cff2 --- /dev/null +++ b/client/src/components/SorteableDataTable/SortableDataTableToolbar.tsx @@ -0,0 +1,212 @@ +import { + Button, + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, + Popover, + PopoverContent, + PopoverTrigger, + Separator, + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/ui"; +import { Table } from "@tanstack/react-table"; +import { + CalendarIcon, + CirclePlusIcon, + ClockIcon, + CopyPlusIcon, + ForwardIcon, + MoreVerticalIcon, + ReplyAllIcon, + ReplyIcon, + ScanIcon, + Trash2Icon, +} from "lucide-react"; + +export const SortableDataTableToolbar = ({ + table, +}: { + table: Table; +}) => { + const selectedRowsCount = table.getSelectedRowModel().rows.length; + + if (selectedRowsCount) { + return ( +
+
+ + + + + + + Duplica las fila(s) seleccionadas(s) + + + + + + + Elimina las fila(s) seleccionada(s) + + + + + + + Quita la selección + +
+
+ ); + } + + return ( +
+
+ + + + + Añadir fila + + + + + + + + + + + +
+
Snooze until
+
+ + + + +
+
+
+ +
+
+
+ Snooze +
+
+
+ + + + + Reply + + + + + + Reply all + + + + + + Forward + +
+ + + + + + + {table.getAllColumns().map((column) => { + return ( + column.toggleVisibility(!!value)} + > + {column.id} + + ); + })} + + +
+ ); +}; diff --git a/client/src/components/SorteableDataTable/SortableTableRow.tsx b/client/src/components/SorteableDataTable/SortableTableRow.tsx new file mode 100644 index 0000000..ec9e47f --- /dev/null +++ b/client/src/components/SorteableDataTable/SortableTableRow.tsx @@ -0,0 +1,80 @@ +import { cn } from "@/lib/utils"; +import { TableRow } from "@/ui/table"; +import { DraggableSyntheticListeners } from "@dnd-kit/core"; +import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { + CSSProperties, + PropsWithChildren, + createContext, + useMemo, +} from "react"; +import { SortableProps } from "./SortableDataTable"; + +interface Context { + attributes: Record; + listeners: DraggableSyntheticListeners; + ref(node: HTMLElement | null): void; +} +export const SortableTableRowContext = createContext({ + attributes: {}, + listeners: undefined, + ref() {}, +}); + +function animateLayoutChanges(args) { + if (args.isSorting || args.wasDragging) { + return defaultAnimateLayoutChanges(args); + } + + return true; +} + +export function SortableTableRow({ + id, + children, +}: PropsWithChildren) { + const { + attributes, + isDragging, + listeners, + setNodeRef, + setActivatorNodeRef, + transform, + transition, + } = useSortable({ + animateLayoutChanges, + id, + }); + + const style: CSSProperties = { + transform: CSS.Translate.toString(transform), + transition, + }; + + const context = useMemo( + () => ({ + attributes, + listeners, + ref: setActivatorNodeRef, + }), + [attributes, listeners, setActivatorNodeRef], + ); + + return ( + + + {children} + + + ); +} diff --git a/client/src/components/SorteableDataTable/index.tsx b/client/src/components/SorteableDataTable/index.tsx new file mode 100644 index 0000000..42efdd6 --- /dev/null +++ b/client/src/components/SorteableDataTable/index.tsx @@ -0,0 +1 @@ +export * from "./SortableDataTable"; diff --git a/client/src/components/index.ts b/client/src/components/index.ts index 8c22577..489ac84 100644 --- a/client/src/components/index.ts +++ b/client/src/components/index.ts @@ -1,8 +1,14 @@ +export * from "./ButtonGroup"; export * from "./Container"; +export * from "./CustomButtons"; export * from "./CustomDialog"; +export * from "./DataTable"; +export * from "./EmptyState"; +export * from "./ErrorOverlay"; export * from "./Forms"; export * from "./Layout"; export * from "./LoadingIndicator"; export * from "./LoadingOverlay"; export * from "./ProtectedRoute"; +export * from "./SorteableDataTable"; export * from "./TailwindIndicator"; diff --git a/client/src/lib/hooks/index.ts b/client/src/lib/hooks/index.ts index 1b35fd9..eb78d60 100644 --- a/client/src/lib/hooks/index.ts +++ b/client/src/lib/hooks/index.ts @@ -1,6 +1,6 @@ /*export * from "./useBreadcrumbs"; export * from "./useDataSource"; -export * from "./useDataTable"; + export * from "./useForm"; export * from "./useLoadingOvertime"; export * from "./useMounted"; @@ -13,4 +13,6 @@ export * from "./useUrlId"; export * from "./useAuth"; export * from "./useCustomDialog"; +export * from "./useDataTable"; +export * from "./usePagination"; export * from "./useTheme"; diff --git a/client/src/lib/hooks/useDataSource/DataSourceContext.tsx b/client/src/lib/hooks/useDataSource/DataSourceContext.tsx index 19f47d5..fe85462 100644 --- a/client/src/lib/hooks/useDataSource/DataSourceContext.tsx +++ b/client/src/lib/hooks/useDataSource/DataSourceContext.tsx @@ -1,7 +1,7 @@ import { PropsWithChildren, createContext } from "react"; import { IDataSource } from "./DataSource"; -export const DataSourceContext = createContext(null); +export const DataSourceContext = createContext(undefined); export const DataSourceProvider = ({ dataSource, diff --git a/client/src/lib/hooks/useDataTable/DataTableContext.tsx b/client/src/lib/hooks/useDataTable/DataTableContext.tsx new file mode 100644 index 0000000..422563a --- /dev/null +++ b/client/src/lib/hooks/useDataTable/DataTableContext.tsx @@ -0,0 +1,21 @@ +import { usePaginationParams } from "@/lib/hooks"; +import { PropsWithChildren, createContext } from "react"; + +export interface IDataTableContextState {} + +export const DataTableContext = createContext(null); + +export const DataTableProvider = ({ children }: PropsWithChildren) => { + const [pagination, setPagination] = usePaginationParams(); + + return ( + + {children} + + ); +}; diff --git a/client/src/lib/hooks/useDataTable/index.ts b/client/src/lib/hooks/useDataTable/index.ts index a625e52..0820968 100644 --- a/client/src/lib/hooks/useDataTable/index.ts +++ b/client/src/lib/hooks/useDataTable/index.ts @@ -1,3 +1,5 @@ +export * from "./DataTableContext"; export * from "./useDataTable"; export * from "./useDataTableColumns"; +export * from "./useDataTableContext"; export * from "./useQueryDataTable"; diff --git a/client/src/lib/hooks/useDataTable/types.ts b/client/src/lib/hooks/useDataTable/types.ts new file mode 100644 index 0000000..85d5218 --- /dev/null +++ b/client/src/lib/hooks/useDataTable/types.ts @@ -0,0 +1,12 @@ +export interface Option { + label: string; + value: string; + icon?: React.ComponentType<{ className?: string }>; +} + +export interface DataTableFilterField { + label: string; + value: keyof TData; + placeholder?: string; + options?: Option[]; +} diff --git a/client/src/lib/hooks/useDataTable/useDataTable.tsx b/client/src/lib/hooks/useDataTable/useDataTable.tsx index d8ce4a6..b735289 100644 --- a/client/src/lib/hooks/useDataTable/useDataTable.tsx +++ b/client/src/lib/hooks/useDataTable/useDataTable.tsx @@ -16,8 +16,8 @@ import { import { getDataTableSelectionColumn } from "@/components"; import React, { useCallback, useMemo, useState } from "react"; import { useSearchParams } from "react-router-dom"; -import { DataTableFilterField } from "../../types"; -import { usePaginationParams } from "../usePagination"; +import { DataTableFilterField } from "./types"; +import { useDataTableContext } from "./useDataTableContext"; //import { useDebounce } from "@/hooks/use-debounce"; @@ -110,13 +110,17 @@ export function useDataTable({ enableSorting = false, enableHiding = false, enableRowSelection = false, + onPaginationChange, filterFields = [], + // eslint-disable-next-line @typescript-eslint/no-unused-vars enableAdvancedFilter = false, }: UseDataTableProps) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [searchParams, setSearchParams] = useSearchParams(); - const [pagination, setPagination] = usePaginationParams(); + const { pagination, setPagination } = useDataTableContext(); + console.log("pagination TABLA =>", pagination); + const [sorting, setSorting] = useState([]); // Memoize computation of searchableColumns and filterableColumns @@ -149,12 +153,8 @@ export function useDataTable({ const initialColumnFilters: ColumnFiltersState = useMemo(() => { return Array.from(searchParams.entries()).reduce( (filters, [key, value]) => { - const filterableColumn = filterableColumns.find( - (column) => column.value === key - ); - const searchableColumn = searchableColumns.find( - (column) => column.value === key - ); + const filterableColumn = filterableColumns.find((column) => column.value === key); + const searchableColumn = searchableColumns.find((column) => column.value === key); if (filterableColumn) { filters.push({ @@ -177,8 +177,7 @@ export function useDataTable({ // Table states const [rowSelection, setRowSelection] = React.useState({}); - const [columnVisibility, setColumnVisibility] = - React.useState({}); + const [columnVisibility, setColumnVisibility] = React.useState({}); const [columnFilters, setColumnFilters] = React.useState(initialColumnFilters); @@ -186,7 +185,6 @@ export function useDataTable({ const paginationUpdater: OnChangeFn = (updater) => { if (typeof updater === "function") { const newPagination = updater(pagination); - console.log(newPagination); setPagination(newPagination); } }; diff --git a/client/src/lib/hooks/useDataTable/useDataTableContext.tsx b/client/src/lib/hooks/useDataTable/useDataTableContext.tsx new file mode 100644 index 0000000..30aa15b --- /dev/null +++ b/client/src/lib/hooks/useDataTable/useDataTableContext.tsx @@ -0,0 +1,9 @@ +import { useContext } from "react"; +import { DataTableContext } from "./DataTableContext"; + +export const useDataTableContext = () => { + const context = useContext(DataTableContext); + if (context === null) + throw new Error("useDataTableContext must be used within a DataTableProvider"); + return context; +}; diff --git a/client/src/lib/hooks/usePagination/usePagination.test.ts b/client/src/lib/hooks/usePagination/usePagination.test.ts new file mode 100644 index 0000000..6641746 --- /dev/null +++ b/client/src/lib/hooks/usePagination/usePagination.test.ts @@ -0,0 +1,75 @@ +import { act, renderHook } from "@testing-library/react-hooks"; +import { + INITIAL_PAGE_INDEX, + INITIAL_PAGE_SIZE, + MAX_PAGE_SIZE, + MIN_PAGE_SIZE, + PaginationState, + usePagination, +} from "./usePagination"; + +describe("usePagination", () => { + it("should initialize with default values", () => { + const { result } = renderHook(() => usePagination()); + const [pagination] = result.current; + + expect(pagination.pageIndex).toBe(INITIAL_PAGE_INDEX); + expect(pagination.pageSize).toBe(INITIAL_PAGE_SIZE); + }); + + it("should initialize with custom values", () => { + const customPageIndex = 2; + const customPageSize = 20; + const { result } = renderHook(() => usePagination(customPageIndex, customPageSize)); + const [pagination] = result.current; + + expect(pagination.pageIndex).toBe(customPageIndex); + expect(pagination.pageSize).toBe(customPageSize); + }); + + it("should update pagination state correctly", () => { + const { result } = renderHook(() => usePagination()); + const [pagination, updatePagination] = result.current; + + const newPagination: PaginationState = { pageIndex: 1, pageSize: 25 }; + + act(() => { + updatePagination(newPagination); + }); + + expect(pagination.pageIndex).toBe(newPagination.pageIndex); + expect(pagination.pageSize).toBe(newPagination.pageSize); + }); + + it("should not update pageIndex below INITIAL_PAGE_INDEX", () => { + const { result } = renderHook(() => usePagination()); + const [, updatePagination] = result.current; + + act(() => { + updatePagination({ pageIndex: -1, pageSize: INITIAL_PAGE_SIZE }); + }); + + const [pagination] = result.current; + + expect(pagination.pageIndex).toBe(INITIAL_PAGE_INDEX); + }); + + it("should not update pageSize below MIN_PAGE_SIZE or above MAX_PAGE_SIZE", () => { + const { result } = renderHook(() => usePagination()); + const [, updatePagination] = result.current; + + act(() => { + updatePagination({ pageIndex: INITIAL_PAGE_INDEX, pageSize: MIN_PAGE_SIZE - 1 }); + }); + + let [pagination] = result.current; + expect(pagination.pageSize).toBe(INITIAL_PAGE_SIZE); // No cambia porque pageSize es menor que el mínimo + + act(() => { + updatePagination({ pageIndex: INITIAL_PAGE_INDEX, pageSize: MAX_PAGE_SIZE + 1 }); + }); + + [pagination] = result.current; + expect(pagination.pageSize).toBe(INITIAL_PAGE_SIZE); // No cambia porque pageSize es mayor que el máximo + }); +}); diff --git a/client/src/lib/hooks/usePagination/usePagination.tsx b/client/src/lib/hooks/usePagination/usePagination.tsx index 4786539..b290384 100644 --- a/client/src/lib/hooks/usePagination/usePagination.tsx +++ b/client/src/lib/hooks/usePagination/usePagination.tsx @@ -6,7 +6,7 @@ export const INITIAL_PAGE_SIZE = 10; export const MIN_PAGE_INDEX = 0; export const MIN_PAGE_SIZE = 1; -export const MAX_PAGE_SIZE = Number.MAX_SAFE_INTEGER; +export const MAX_PAGE_SIZE = 100; //Number.MAX_SAFE_INTEGER; export const DEFAULT_PAGE_SIZES = [10, 25, 50, 100]; @@ -31,19 +31,23 @@ export const usePagination = ( const updatePagination = (newPagination: PaginationState) => { // Realiza comprobaciones antes de actualizar el estado + const validatedPagination = newPagination; - if (newPagination.pageIndex < INITIAL_PAGE_INDEX) { - newPagination.pageIndex = INITIAL_PAGE_INDEX; + if (validatedPagination.pageIndex < INITIAL_PAGE_INDEX) { + validatedPagination.pageIndex = INITIAL_PAGE_INDEX; } - if ( - newPagination.pageSize < MIN_PAGE_SIZE || - newPagination.pageSize > MAX_PAGE_SIZE - ) { - return; + if (newPagination.pageSize < MIN_PAGE_SIZE || newPagination.pageSize > MAX_PAGE_SIZE) { + validatedPagination.pageSize = MIN_PAGE_SIZE; } - setPagination(newPagination); + setPagination((oldPagination) => ({ + ...oldPagination, + pageIndex: newPagination.pageIndex, + pageSize: newPagination.pageSize, + })); + + return validatedPagination; }; return [pagination, updatePagination] as const; diff --git a/client/src/lib/hooks/usePagination/usePaginationParams.tsx b/client/src/lib/hooks/usePagination/usePaginationParams.tsx index 6122be0..dd24df0 100644 --- a/client/src/lib/hooks/usePagination/usePaginationParams.tsx +++ b/client/src/lib/hooks/usePagination/usePaginationParams.tsx @@ -1,8 +1,11 @@ -import { useEffect, useMemo } from "react"; +import { PaginationState } from "@tanstack/react-table"; +import { useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE, + MAX_PAGE_SIZE, + MIN_PAGE_SIZE, usePagination, } from "./usePagination"; @@ -17,24 +20,29 @@ export const usePaginationParams = ( const calculatedPageIndex = useMemo(() => { const parsedPageIndex = parseInt(urlParamPageIndex ?? "", 10); - return !isNaN(parsedPageIndex) ? parsedPageIndex : initialPageIndex; + let result = !isNaN(parsedPageIndex) ? parsedPageIndex : initialPageIndex; + + if (result < initialPageIndex) { + result = initialPageIndex; + } + + return result; }, [urlParamPageIndex, initialPageIndex]); const calculatedPageSize = useMemo(() => { const parsedPageSize = parseInt(urlParamPageSize ?? "", 10); - return !isNaN(parsedPageSize) ? parsedPageSize : initialPageSize; + let result = !isNaN(parsedPageSize) ? parsedPageSize : initialPageSize; + if (result < MIN_PAGE_SIZE || result > MAX_PAGE_SIZE) { + result = initialPageSize; + } + return result; }, [urlParamPageSize, initialPageSize]); - const [pagination, setPagination] = usePagination( - calculatedPageIndex, - calculatedPageSize - ); + const [pagination, setPagination] = usePagination(calculatedPageIndex, calculatedPageSize); - useEffect(() => { + /*useEffect(() => { // Actualizar la URL cuando cambia la paginación - const actualSearchParam = Object.fromEntries( - new URLSearchParams(urlSearchParams) - ); + const actualSearchParam = Object.fromEntries(new URLSearchParams(urlSearchParams)); if ( String(pagination.pageIndex) !== actualSearchParam.page_index || @@ -46,7 +54,17 @@ export const usePaginationParams = ( page_size: String(pagination.pageSize), }); } - }, [pagination]); + }, [pagination]);*/ - return [pagination, setPagination] as const; + const updatePagination = (newPagination: PaginationState) => { + const _validatedPagination = setPagination(newPagination); + + setUrlSearchParams({ + //...actualSearchParam, + page_index: String(_validatedPagination.pageIndex), + page_size: String(_validatedPagination.pageSize), + }); + }; + + return [pagination, updatePagination]; }; diff --git a/client/src/lib/hooks/useTranslation/useTranslation.tsx b/client/src/lib/hooks/useTranslation/useTranslation.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 1f82bbc..80fcd4e 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1,14 +1,36 @@ { "translation": { - "title": "Presupuestador para distribuidores", + "common": { + "cancel": "Cancel", + "no": "No", + "yes": "Yes", + "Accept": "Accept" + }, + "main_menu": { + "home": "Home", + "settings": "Settings", + "dealers": "Dealers", + "catalog": "Catalog", + "quotes": "Quotes", + "search_placeholder": "Search product, quotes, etc...", + "user": { + "user_menu": "User menu", + "my_account": "My account", + "profile": "Profile", + "settings": "Settings", + "support": "Support", + "logout": "Logout" + }, + "logout": {} + }, "login_page": { - "title": "Presupuestador para distribuidores", - "description": "Introduzca su dirección de correo electrónico y contraseña para acceder", + "title": "Uecko Quotes", + "description": "Enter your email address and password to login", "email_label": "Email", - "email_placeholder": "micorreo@ejemplo.com", + "email_placeholder": "user@sample.com", "password_label": "Password", - "forgotten_password": "¿Has olvidado tu contraseña?", - "become_dealer": "¿Quieres ser distribuidor de Uecko?", + "forgotten_password": "Forgot your password?", + "become_dealer": "Do you want to become a Uecko dealer?", "contact_us": "Contact us", "login": "Login", "error": "Error" diff --git a/client/src/locales/es.json b/client/src/locales/es.json index 5953046..87d6479 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -12,7 +12,7 @@ "dealers": "Distribuidores", "catalog": "Catálogo", "quotes": "Cotizaciones", - "search_placeholder": "Buscar productos, distribuidores, etc...", + "search_placeholder": "Buscar productos, cotizaciones, etc...", "user": { "user_menu": "Menú del usuario", "my_account": "Mi cuenta", @@ -34,6 +34,12 @@ "contact_us": "Contacta con nosotros", "login": "Entrar", "error": "Error" + }, + "dashboard": { + "welcome": "Bienvenido" + }, + "catalog": { + "title": "Catálogo de artículos" } } } diff --git a/client/src/lib/hooks/useTranslation/index.ts b/client/src/setupTests.js similarity index 100% rename from client/src/lib/hooks/useTranslation/index.ts rename to client/src/setupTests.js