.
This commit is contained in:
parent
090f8acf5c
commit
e6498b4104
13
client/jest.config.ts
Normal file
13
client/jest.config.ts
Normal file
@ -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)$": "<rootDir>/test/__ mocks __/fileMock.js",
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -8,9 +8,12 @@
|
|||||||
"dev": "vite --host",
|
"dev": "vite --host",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.1.0",
|
||||||
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@hookform/resolvers": "^3.5.0",
|
"@hookform/resolvers": "^3.5.0",
|
||||||
"@radix-ui/react-accordion": "^1.1.2",
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
@ -40,6 +43,7 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@tanstack/react-query": "^5.39.0",
|
"@tanstack/react-query": "^5.39.0",
|
||||||
|
"@tanstack/react-table": "^8.17.3",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
@ -62,6 +66,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/react-query-devtools": "^5.39.0",
|
"@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/node": "^20.14.0",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
@ -73,10 +81,15 @@
|
|||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
"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": "^8.4.38",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
|
"ts-jest": "^29.1.4",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.2.12"
|
"vite": "^5.2.12"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
import { Outlet, RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||||
import { LoginPage, LogoutPage, SettingsPage, StartPage } from "./app";
|
import { DealerLayout, DealersList, LoginPage, LogoutPage, SettingsPage, StartPage } from "./app";
|
||||||
import { CatalogList } from "./app/catalog";
|
import { CatalogLayout, CatalogList } from "./app/catalog";
|
||||||
import { DashboardPage } from "./app/dashboard";
|
import { DashboardPage } from "./app/dashboard";
|
||||||
import { DealersList } from "./app/dealers/list";
|
|
||||||
import { QuotesList } from "./app/quotes/list";
|
import { QuotesList } from "./app/quotes/list";
|
||||||
import { ProtectedRoute } from "./components";
|
import { ProtectedRoute } from "./components";
|
||||||
|
|
||||||
@ -29,17 +28,33 @@ export const Routes = () => {
|
|||||||
path: "/catalog",
|
path: "/catalog",
|
||||||
element: (
|
element: (
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<CatalogList />
|
<CatalogLayout>
|
||||||
|
<Outlet />
|
||||||
|
</CatalogLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <CatalogList />,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/dealers",
|
path: "/dealers",
|
||||||
element: (
|
element: (
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<DealersList />
|
<DealerLayout>
|
||||||
|
<Outlet />
|
||||||
|
</DealerLayout>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <DealersList />,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/quotes",
|
path: "/quotes",
|
||||||
|
|||||||
54
client/src/app/catalog/CatalogActions.ts
Normal file
54
client/src/app/catalog/CatalogActions.ts
Normal file
@ -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<IListResponse_DTO<IListCustomerInvoices_Response_DTO>> => {
|
||||||
|
return dataProvider.getList({
|
||||||
|
resource: "invoices",
|
||||||
|
quickSearchTerm,
|
||||||
|
pagination,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
21
client/src/app/catalog/CatalogContext.tsx
Normal file
21
client/src/app/catalog/CatalogContext.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { usePagination } from "@/lib/hooks";
|
||||||
|
import { PropsWithChildren, createContext } from "react";
|
||||||
|
|
||||||
|
export interface ICatalogContextState {}
|
||||||
|
|
||||||
|
export const CatalogContext = createContext<ICatalogContextState | null>(null);
|
||||||
|
|
||||||
|
export const CatalogProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const [pagination, setPagination] = usePagination();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CatalogContext.Provider
|
||||||
|
value={{
|
||||||
|
pagination,
|
||||||
|
setPagination,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CatalogContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
36
client/src/app/catalog/components/CatalogDataTable.tsx
Normal file
36
client/src/app/catalog/components/CatalogDataTable.tsx
Normal file
@ -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<IListArticles_Response_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 (
|
||||||
|
<>
|
||||||
|
<DataTable table={table} paginationOptions={{ visible: true }} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
client/src/app/catalog/components/index.ts
Normal file
1
client/src/app/catalog/components/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./CatalogDataTable";
|
||||||
77
client/src/app/catalog/components/useCatalogTableColumns.tsx
Normal file
77
client/src/app/catalog/components/useCatalogTableColumns.tsx
Normal file
@ -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<IListArticles_Response_DTO>
|
||||||
|
): ColumnDef<IListArticles_Response_DTO>[] => {
|
||||||
|
const customerColumns: ColumnDef<IListArticles_Response_DTO>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
/*{
|
||||||
|
id: "complete_name",
|
||||||
|
header: "Nombre",
|
||||||
|
accessorFn: (row) => (
|
||||||
|
<div className="flex items-center justify-between space-x-4">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Avatar>
|
||||||
|
<AvatarImage src={row.photo_url} />
|
||||||
|
<AvatarFallback>
|
||||||
|
{acronym(`${row.first_name} ${row.last_name}`)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<p className="text-base font-semibold">{`${row.first_name} ${row.last_name}`}</p>
|
||||||
|
<p className="mt-1">{row.job_title}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
enableSorting: true,
|
||||||
|
sortingFn: "alphanumeric",
|
||||||
|
enableHiding: false,
|
||||||
|
|
||||||
|
cell: ({ renderValue }) => (
|
||||||
|
<span className="w-full">
|
||||||
|
<>{renderValue()}</>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},*/
|
||||||
|
{
|
||||||
|
id: "company",
|
||||||
|
accessorKey: "company_name",
|
||||||
|
header: "Compañía",
|
||||||
|
enableSorting: true,
|
||||||
|
sortingFn: "alphanumeric",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "state",
|
||||||
|
accessorKey: "state",
|
||||||
|
header: "Estado",
|
||||||
|
cell: ({ renderValue }) => (
|
||||||
|
<Badge variant={"destructive"}>
|
||||||
|
<>{renderValue()}</>
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "phone",
|
||||||
|
accessorKey: "phone",
|
||||||
|
header: "Phone",
|
||||||
|
enableSorting: true,
|
||||||
|
sortingFn: "alphanumeric",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: "Acciones",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <DataTableRowActions row={row} actions={actions} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[actions]
|
||||||
|
);
|
||||||
|
return customerColumns;
|
||||||
|
};
|
||||||
1
client/src/app/catalog/hooks/index.ts
Normal file
1
client/src/app/catalog/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./useCatalogList";
|
||||||
40
client/src/app/catalog/hooks/useCatalogList.tsx
Normal file
40
client/src/app/catalog/hooks/useCatalogList.tsx
Normal file
@ -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<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseCatalogListResponse = UseListQueryResult<
|
||||||
|
IListResponse_DTO<IListArticles_Response_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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1 +1,2 @@
|
|||||||
|
export * from "./layout";
|
||||||
export * from "./list";
|
export * from "./list";
|
||||||
|
|||||||
17
client/src/app/catalog/layout.tsx
Normal file
17
client/src/app/catalog/layout.tsx
Normal file
@ -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 (
|
||||||
|
<CatalogProvider>
|
||||||
|
<Layout>
|
||||||
|
<LayoutHeader />
|
||||||
|
<LayoutContent>
|
||||||
|
<DataTableProvider>{children}</DataTableProvider>
|
||||||
|
</LayoutContent>
|
||||||
|
</Layout>
|
||||||
|
</CatalogProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -28,289 +28,383 @@ import {
|
|||||||
|
|
||||||
import { File, ListFilter, MoreHorizontal, PlusCircle } from "lucide-react";
|
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 = () => {
|
export const CatalogList = () => {
|
||||||
return (
|
const navigate = useNavigate();
|
||||||
<Layout>
|
|
||||||
<LayoutHeader />
|
|
||||||
<LayoutContent>
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<h1 className='text-lg font-semibold md:text-2xl'>Catalog</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs defaultValue='all'>
|
const { pagination } = useDataTableContext();
|
||||||
<div className='flex items-center'>
|
console.log("pagination PADRE => ", pagination);
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value='all'>All</TabsTrigger>
|
const { data, isPending, isError, error, refetch } = useCatalogList({
|
||||||
<TabsTrigger value='active'>Active</TabsTrigger>
|
pagination: {
|
||||||
<TabsTrigger value='draft'>Draft</TabsTrigger>
|
pageIndex: pagination.pageIndex,
|
||||||
<TabsTrigger value='archived' className='hidden sm:flex'>
|
pageSize: pagination.pageSize,
|
||||||
Archived
|
},
|
||||||
</TabsTrigger>
|
});
|
||||||
</TabsList>
|
|
||||||
<div className='flex items-center gap-2 ml-auto'>
|
const columns = useMemo(
|
||||||
<DropdownMenu>
|
() => [
|
||||||
<DropdownMenuTrigger asChild>
|
{
|
||||||
<Button variant='outline' size='sm' className='h-8 gap-1'>
|
id: "description" as const,
|
||||||
<ListFilter className='h-3.5 w-3.5' />
|
accessorKey: "description",
|
||||||
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Filter</span>
|
size: 400,
|
||||||
</Button>
|
enableHiding: false,
|
||||||
</DropdownMenuTrigger>
|
enableSorting: false,
|
||||||
<DropdownMenuContent align='end'>
|
enableResizing: false,
|
||||||
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
|
},
|
||||||
<DropdownMenuSeparator />
|
],
|
||||||
<DropdownMenuCheckboxItem checked>Active</DropdownMenuCheckboxItem>
|
[]
|
||||||
<DropdownMenuCheckboxItem>Draft</DropdownMenuCheckboxItem>
|
);
|
||||||
<DropdownMenuCheckboxItem>Archived</DropdownMenuCheckboxItem>
|
|
||||||
</DropdownMenuContent>
|
const { table } = useDataTable({
|
||||||
</DropdownMenu>
|
data: data?.items ?? [],
|
||||||
<Button size='sm' variant='outline' className='h-8 gap-1'>
|
columns: columns,
|
||||||
<File className='h-3.5 w-3.5' />
|
pageCount: data?.total_pages ?? -1,
|
||||||
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Export</span>
|
});
|
||||||
</Button>
|
|
||||||
<Button size='sm' className='h-8 gap-1'>
|
return <DataTable table={table} paginationOptions={{ visible: true }} />;
|
||||||
<PlusCircle className='h-3.5 w-3.5' />
|
|
||||||
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Add Product</span>
|
if (isError || isPending) {
|
||||||
</Button>
|
return <></>;
|
||||||
</div>
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setPagination({
|
||||||
|
pageIndex: pagination.pageIndex + 1,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ Página
|
||||||
|
</Button>
|
||||||
|
{data.items.map((row) => (
|
||||||
|
<p>{row.description}</p>
|
||||||
|
))}
|
||||||
|
<CatalogDataTable data={data} />;
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return <ErrorOverlay subtitle={(error as Error).message} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<Card x-chunk='dashboard-06-chunk-0'>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
<Trans i18nKey='catalog.title' />
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Manage your products and view their sales performance.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<DataTableSkeleton
|
||||||
|
columnCount={6}
|
||||||
|
searchableColumnCount={1}
|
||||||
|
filterableColumnCount={2}
|
||||||
|
//cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem"]}
|
||||||
|
shrinkZero
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?.total_items === 0) {
|
||||||
|
return (
|
||||||
|
<SimpleEmptyState
|
||||||
|
subtitle='Empieza cargando los artículos del catálogo'
|
||||||
|
buttonText=''
|
||||||
|
onButtonClick={() => navigate("/catalog/add")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tabs defaultValue='all'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value='all'>All</TabsTrigger>
|
||||||
|
<TabsTrigger value='active'>Active</TabsTrigger>
|
||||||
|
<TabsTrigger value='draft'>Draft</TabsTrigger>
|
||||||
|
<TabsTrigger value='archived' className='hidden sm:flex'>
|
||||||
|
Archived
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div className='flex items-center gap-2 ml-auto'>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant='outline' size='sm' className='h-8 gap-1'>
|
||||||
|
<ListFilter className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Filter</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuCheckboxItem checked>Active</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem>Draft</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem>Archived</DropdownMenuCheckboxItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Button size='sm' variant='outline' className='h-8 gap-1'>
|
||||||
|
<File className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Export</span>
|
||||||
|
</Button>
|
||||||
|
<Button size='sm' className='h-8 gap-1'>
|
||||||
|
<PlusCircle className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Add Product</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<TabsContent value='all'>
|
</div>
|
||||||
<Card x-chunk='dashboard-06-chunk-0'>
|
<TabsContent value='all'>
|
||||||
<CardHeader>
|
<Card x-chunk='dashboard-06-chunk-0'>
|
||||||
<CardTitle>Products</CardTitle>
|
<CardHeader>
|
||||||
<CardDescription>
|
<CardTitle>
|
||||||
Manage your products and view their sales performance.
|
<Trans i18nKey='catalog.title' />
|
||||||
</CardDescription>
|
</CardTitle>
|
||||||
</CardHeader>
|
<CardDescription>
|
||||||
<CardContent>
|
Manage your products and view their sales performance.
|
||||||
<Table>
|
</CardDescription>
|
||||||
<TableHeader>
|
</CardHeader>
|
||||||
<TableRow>
|
<CardContent>
|
||||||
<TableHead className='hidden w-[100px] sm:table-cell'>
|
<Table>
|
||||||
<span className='sr-only'>Image</span>
|
<TableHeader>
|
||||||
</TableHead>
|
<TableRow>
|
||||||
<TableHead>Name</TableHead>
|
<TableHead className='hidden w-[100px] sm:table-cell'>
|
||||||
<TableHead>Status</TableHead>
|
<span className='sr-only'>Image</span>
|
||||||
<TableHead className='hidden md:table-cell'>Price</TableHead>
|
</TableHead>
|
||||||
<TableHead className='hidden md:table-cell'>Total Sales</TableHead>
|
<TableHead>Name</TableHead>
|
||||||
<TableHead className='hidden md:table-cell'>Created at</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead>
|
<TableHead className='hidden md:table-cell'>Price</TableHead>
|
||||||
<span className='sr-only'>Actions</span>
|
<TableHead className='hidden md:table-cell'>Total Sales</TableHead>
|
||||||
</TableHead>
|
<TableHead className='hidden md:table-cell'>Created at</TableHead>
|
||||||
</TableRow>
|
<TableHead>
|
||||||
</TableHeader>
|
<span className='sr-only'>Actions</span>
|
||||||
<TableBody>
|
</TableHead>
|
||||||
<TableRow>
|
</TableRow>
|
||||||
<TableCell className='hidden sm:table-cell'>
|
</TableHeader>
|
||||||
<img
|
<TableBody>
|
||||||
alt='Product image'
|
<TableRow>
|
||||||
className='object-cover rounded-md aspect-square'
|
<TableCell className='hidden sm:table-cell'>
|
||||||
height='64'
|
<img
|
||||||
src='/placeholder.svg'
|
alt='Product image'
|
||||||
width='64'
|
className='object-cover rounded-md aspect-square'
|
||||||
/>
|
height='64'
|
||||||
</TableCell>
|
src='/placeholder.svg'
|
||||||
<TableCell className='font-medium'>Laser Lemonade Machine</TableCell>
|
width='64'
|
||||||
<TableCell>
|
/>
|
||||||
<Badge variant='outline'>Draft</Badge>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell className='font-medium'>Laser Lemonade Machine</TableCell>
|
||||||
<TableCell className='hidden md:table-cell'>$499.99</TableCell>
|
<TableCell>
|
||||||
<TableCell className='hidden md:table-cell'>25</TableCell>
|
<Badge variant='outline'>Draft</Badge>
|
||||||
<TableCell className='hidden md:table-cell'>2023-07-12 10:42 AM</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className='hidden md:table-cell'>$499.99</TableCell>
|
||||||
<DropdownMenu>
|
<TableCell className='hidden md:table-cell'>25</TableCell>
|
||||||
<DropdownMenuTrigger asChild>
|
<TableCell className='hidden md:table-cell'>2023-07-12 10:42 AM</TableCell>
|
||||||
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
<TableCell>
|
||||||
<MoreHorizontal className='w-4 h-4' />
|
<DropdownMenu>
|
||||||
<span className='sr-only'>Toggle menu</span>
|
<DropdownMenuTrigger asChild>
|
||||||
</Button>
|
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
||||||
</DropdownMenuTrigger>
|
<MoreHorizontal className='w-4 h-4' />
|
||||||
<DropdownMenuContent align='end'>
|
<span className='sr-only'>Toggle menu</span>
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
</Button>
|
||||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
<DropdownMenuContent align='end'>
|
||||||
</DropdownMenuContent>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
</DropdownMenu>
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
</TableCell>
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
</TableRow>
|
</DropdownMenuContent>
|
||||||
<TableRow>
|
</DropdownMenu>
|
||||||
<TableCell className='hidden sm:table-cell'>
|
</TableCell>
|
||||||
<img
|
</TableRow>
|
||||||
alt='Product image'
|
<TableRow>
|
||||||
className='object-cover rounded-md aspect-square'
|
<TableCell className='hidden sm:table-cell'>
|
||||||
height='64'
|
<img
|
||||||
src='/placeholder.svg'
|
alt='Product image'
|
||||||
width='64'
|
className='object-cover rounded-md aspect-square'
|
||||||
/>
|
height='64'
|
||||||
</TableCell>
|
src='/placeholder.svg'
|
||||||
<TableCell className='font-medium'>Hypernova Headphones</TableCell>
|
width='64'
|
||||||
<TableCell>
|
/>
|
||||||
<Badge variant='outline'>Active</Badge>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell className='font-medium'>Hypernova Headphones</TableCell>
|
||||||
<TableCell className='hidden md:table-cell'>$129.99</TableCell>
|
<TableCell>
|
||||||
<TableCell className='hidden md:table-cell'>100</TableCell>
|
<Badge variant='outline'>Active</Badge>
|
||||||
<TableCell className='hidden md:table-cell'>2023-10-18 03:21 PM</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className='hidden md:table-cell'>$129.99</TableCell>
|
||||||
<DropdownMenu>
|
<TableCell className='hidden md:table-cell'>100</TableCell>
|
||||||
<DropdownMenuTrigger asChild>
|
<TableCell className='hidden md:table-cell'>2023-10-18 03:21 PM</TableCell>
|
||||||
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
<TableCell>
|
||||||
<MoreHorizontal className='w-4 h-4' />
|
<DropdownMenu>
|
||||||
<span className='sr-only'>Toggle menu</span>
|
<DropdownMenuTrigger asChild>
|
||||||
</Button>
|
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
||||||
</DropdownMenuTrigger>
|
<MoreHorizontal className='w-4 h-4' />
|
||||||
<DropdownMenuContent align='end'>
|
<span className='sr-only'>Toggle menu</span>
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
</Button>
|
||||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
<DropdownMenuContent align='end'>
|
||||||
</DropdownMenuContent>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
</DropdownMenu>
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
</TableCell>
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
</TableRow>
|
</DropdownMenuContent>
|
||||||
<TableRow>
|
</DropdownMenu>
|
||||||
<TableCell className='hidden sm:table-cell'>
|
</TableCell>
|
||||||
<img
|
</TableRow>
|
||||||
alt='Product image'
|
<TableRow>
|
||||||
className='object-cover rounded-md aspect-square'
|
<TableCell className='hidden sm:table-cell'>
|
||||||
height='64'
|
<img
|
||||||
src='/placeholder.svg'
|
alt='Product image'
|
||||||
width='64'
|
className='object-cover rounded-md aspect-square'
|
||||||
/>
|
height='64'
|
||||||
</TableCell>
|
src='/placeholder.svg'
|
||||||
<TableCell className='font-medium'>AeroGlow Desk Lamp</TableCell>
|
width='64'
|
||||||
<TableCell>
|
/>
|
||||||
<Badge variant='outline'>Active</Badge>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell className='font-medium'>AeroGlow Desk Lamp</TableCell>
|
||||||
<TableCell className='hidden md:table-cell'>$39.99</TableCell>
|
<TableCell>
|
||||||
<TableCell className='hidden md:table-cell'>50</TableCell>
|
<Badge variant='outline'>Active</Badge>
|
||||||
<TableCell className='hidden md:table-cell'>2023-11-29 08:15 AM</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className='hidden md:table-cell'>$39.99</TableCell>
|
||||||
<DropdownMenu>
|
<TableCell className='hidden md:table-cell'>50</TableCell>
|
||||||
<DropdownMenuTrigger asChild>
|
<TableCell className='hidden md:table-cell'>2023-11-29 08:15 AM</TableCell>
|
||||||
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
<TableCell>
|
||||||
<MoreHorizontal className='w-4 h-4' />
|
<DropdownMenu>
|
||||||
<span className='sr-only'>Toggle menu</span>
|
<DropdownMenuTrigger asChild>
|
||||||
</Button>
|
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
||||||
</DropdownMenuTrigger>
|
<MoreHorizontal className='w-4 h-4' />
|
||||||
<DropdownMenuContent align='end'>
|
<span className='sr-only'>Toggle menu</span>
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
</Button>
|
||||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
<DropdownMenuContent align='end'>
|
||||||
</DropdownMenuContent>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
</DropdownMenu>
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
</TableCell>
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
</TableRow>
|
</DropdownMenuContent>
|
||||||
<TableRow>
|
</DropdownMenu>
|
||||||
<TableCell className='hidden sm:table-cell'>
|
</TableCell>
|
||||||
<img
|
</TableRow>
|
||||||
alt='Product image'
|
<TableRow>
|
||||||
className='object-cover rounded-md aspect-square'
|
<TableCell className='hidden sm:table-cell'>
|
||||||
height='64'
|
<img
|
||||||
src='/placeholder.svg'
|
alt='Product image'
|
||||||
width='64'
|
className='object-cover rounded-md aspect-square'
|
||||||
/>
|
height='64'
|
||||||
</TableCell>
|
src='/placeholder.svg'
|
||||||
<TableCell className='font-medium'>TechTonic Energy Drink</TableCell>
|
width='64'
|
||||||
<TableCell>
|
/>
|
||||||
<Badge variant='secondary'>Draft</Badge>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell className='font-medium'>TechTonic Energy Drink</TableCell>
|
||||||
<TableCell className='hidden md:table-cell'>$2.99</TableCell>
|
<TableCell>
|
||||||
<TableCell className='hidden md:table-cell'>0</TableCell>
|
<Badge variant='secondary'>Draft</Badge>
|
||||||
<TableCell className='hidden md:table-cell'>2023-12-25 11:59 PM</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className='hidden md:table-cell'>$2.99</TableCell>
|
||||||
<DropdownMenu>
|
<TableCell className='hidden md:table-cell'>0</TableCell>
|
||||||
<DropdownMenuTrigger asChild>
|
<TableCell className='hidden md:table-cell'>2023-12-25 11:59 PM</TableCell>
|
||||||
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
<TableCell>
|
||||||
<MoreHorizontal className='w-4 h-4' />
|
<DropdownMenu>
|
||||||
<span className='sr-only'>Toggle menu</span>
|
<DropdownMenuTrigger asChild>
|
||||||
</Button>
|
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
||||||
</DropdownMenuTrigger>
|
<MoreHorizontal className='w-4 h-4' />
|
||||||
<DropdownMenuContent align='end'>
|
<span className='sr-only'>Toggle menu</span>
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
</Button>
|
||||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
<DropdownMenuContent align='end'>
|
||||||
</DropdownMenuContent>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
</DropdownMenu>
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
</TableCell>
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
</TableRow>
|
</DropdownMenuContent>
|
||||||
<TableRow>
|
</DropdownMenu>
|
||||||
<TableCell className='hidden sm:table-cell'>
|
</TableCell>
|
||||||
<img
|
</TableRow>
|
||||||
alt='Product image'
|
<TableRow>
|
||||||
className='object-cover rounded-md aspect-square'
|
<TableCell className='hidden sm:table-cell'>
|
||||||
height='64'
|
<img
|
||||||
src='/placeholder.svg'
|
alt='Product image'
|
||||||
width='64'
|
className='object-cover rounded-md aspect-square'
|
||||||
/>
|
height='64'
|
||||||
</TableCell>
|
src='/placeholder.svg'
|
||||||
<TableCell className='font-medium'>Gamer Gear Pro Controller</TableCell>
|
width='64'
|
||||||
<TableCell>
|
/>
|
||||||
<Badge variant='outline'>Active</Badge>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell className='font-medium'>Gamer Gear Pro Controller</TableCell>
|
||||||
<TableCell className='hidden md:table-cell'>$59.99</TableCell>
|
<TableCell>
|
||||||
<TableCell className='hidden md:table-cell'>75</TableCell>
|
<Badge variant='outline'>Active</Badge>
|
||||||
<TableCell className='hidden md:table-cell'>2024-01-01 12:00 AM</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className='hidden md:table-cell'>$59.99</TableCell>
|
||||||
<DropdownMenu>
|
<TableCell className='hidden md:table-cell'>75</TableCell>
|
||||||
<DropdownMenuTrigger asChild>
|
<TableCell className='hidden md:table-cell'>2024-01-01 12:00 AM</TableCell>
|
||||||
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
<TableCell>
|
||||||
<MoreHorizontal className='w-4 h-4' />
|
<DropdownMenu>
|
||||||
<span className='sr-only'>Toggle menu</span>
|
<DropdownMenuTrigger asChild>
|
||||||
</Button>
|
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
||||||
</DropdownMenuTrigger>
|
<MoreHorizontal className='w-4 h-4' />
|
||||||
<DropdownMenuContent align='end'>
|
<span className='sr-only'>Toggle menu</span>
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
</Button>
|
||||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
<DropdownMenuContent align='end'>
|
||||||
</DropdownMenuContent>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
</DropdownMenu>
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
</TableCell>
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
</TableRow>
|
</DropdownMenuContent>
|
||||||
<TableRow>
|
</DropdownMenu>
|
||||||
<TableCell className='hidden sm:table-cell'>
|
</TableCell>
|
||||||
<img
|
</TableRow>
|
||||||
alt='Product image'
|
<TableRow>
|
||||||
className='object-cover rounded-md aspect-square'
|
<TableCell className='hidden sm:table-cell'>
|
||||||
height='64'
|
<img
|
||||||
src='/placeholder.svg'
|
alt='Product image'
|
||||||
width='64'
|
className='object-cover rounded-md aspect-square'
|
||||||
/>
|
height='64'
|
||||||
</TableCell>
|
src='/placeholder.svg'
|
||||||
<TableCell className='font-medium'>Luminous VR Headset</TableCell>
|
width='64'
|
||||||
<TableCell>
|
/>
|
||||||
<Badge variant='outline'>Active</Badge>
|
</TableCell>
|
||||||
</TableCell>
|
<TableCell className='font-medium'>Luminous VR Headset</TableCell>
|
||||||
<TableCell className='hidden md:table-cell'>$199.99</TableCell>
|
<TableCell>
|
||||||
<TableCell className='hidden md:table-cell'>30</TableCell>
|
<Badge variant='outline'>Active</Badge>
|
||||||
<TableCell className='hidden md:table-cell'>2024-02-14 02:14 PM</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell className='hidden md:table-cell'>$199.99</TableCell>
|
||||||
<DropdownMenu>
|
<TableCell className='hidden md:table-cell'>30</TableCell>
|
||||||
<DropdownMenuTrigger asChild>
|
<TableCell className='hidden md:table-cell'>2024-02-14 02:14 PM</TableCell>
|
||||||
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
<TableCell>
|
||||||
<MoreHorizontal className='w-4 h-4' />
|
<DropdownMenu>
|
||||||
<span className='sr-only'>Toggle menu</span>
|
<DropdownMenuTrigger asChild>
|
||||||
</Button>
|
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
||||||
</DropdownMenuTrigger>
|
<MoreHorizontal className='w-4 h-4' />
|
||||||
<DropdownMenuContent align='end'>
|
<span className='sr-only'>Toggle menu</span>
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
</Button>
|
||||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
<DropdownMenuContent align='end'>
|
||||||
</DropdownMenuContent>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
</DropdownMenu>
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
</TableCell>
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
</TableRow>
|
</DropdownMenuContent>
|
||||||
</TableBody>
|
</DropdownMenu>
|
||||||
</Table>
|
</TableCell>
|
||||||
</CardContent>
|
</TableRow>
|
||||||
<CardFooter>
|
</TableBody>
|
||||||
<div className='text-xs text-muted-foreground'>
|
</Table>
|
||||||
Showing <strong>1-10</strong> of <strong>32</strong> products
|
</CardContent>
|
||||||
</div>
|
<CardFooter>
|
||||||
</CardFooter>
|
<div className='text-xs text-muted-foreground'>
|
||||||
</Card>
|
Showing <strong>1-10</strong> of <strong>32</strong> products
|
||||||
</TabsContent>
|
</div>
|
||||||
</Tabs>
|
</CardFooter>
|
||||||
</LayoutContent>
|
</Card>
|
||||||
</Layout>
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
8
client/src/app/catalog/useCatalogContext.tsx
Normal file
8
client/src/app/catalog/useCatalogContext.tsx
Normal file
@ -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;
|
||||||
|
};
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { Layout, LayoutContent, LayoutHeader } from "@/components";
|
import { Layout, LayoutContent, LayoutHeader } from "@/components";
|
||||||
|
import { useGetIdentity } from "@/lib/hooks";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
@ -42,383 +43,396 @@ import {
|
|||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@/ui";
|
} from "@/ui";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
export const DashboardPage = () => {
|
export const DashboardPage = () => {
|
||||||
|
const { data, status } = useGetIdentity();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<LayoutHeader />
|
<LayoutHeader />
|
||||||
<LayoutContent className='grid items-start flex-1 gap-4 p-4 sm:px-6 sm:py-0 md:gap-8 lg:grid-cols-3 xl:grid-cols-3'>
|
<LayoutContent>
|
||||||
<div className='grid items-start gap-4 auto-rows-max md:gap-8 lg:col-span-2'>
|
{status === "success" && (
|
||||||
<div className='grid gap-4 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-2 xl:grid-cols-4'>
|
<div className='flex items-center'>
|
||||||
<Card className='sm:col-span-2' x-chunk='dashboard-05-chunk-0'>
|
<h1 className='text-lg font-semibold md:text-2xl'>{`${t("dashboard.welcome")}, ${
|
||||||
<CardHeader className='pb-3'>
|
data?.name
|
||||||
<CardTitle>Tus Cotizaciones</CardTitle>
|
}`}</h1>
|
||||||
<CardDescription className='max-w-lg leading-relaxed text-balance'>
|
</div>
|
||||||
Introducing Our Dynamic Orders Dashboard for Seamless Management and Insightful
|
)}
|
||||||
Analysis.
|
|
||||||
</CardDescription>
|
<div className='grid items-start flex-1 gap-4 p-4 sm:px-6 sm:py-0 md:gap-8 lg:grid-cols-3 xl:grid-cols-3'>
|
||||||
|
<div className='grid items-start gap-4 auto-rows-max md:gap-8 lg:col-span-2'>
|
||||||
|
<div className='grid gap-4 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-2 xl:grid-cols-4'>
|
||||||
|
<Card className='sm:col-span-2' x-chunk='dashboard-05-chunk-0'>
|
||||||
|
<CardHeader className='pb-3'>
|
||||||
|
<CardTitle>Tus Cotizaciones</CardTitle>
|
||||||
|
<CardDescription className='max-w-lg leading-relaxed text-balance'>
|
||||||
|
Introducing Our Dynamic Orders Dashboard for Seamless Management and Insightful
|
||||||
|
Analysis.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter>
|
||||||
|
<Button>Crear nueva cotización</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
<Card x-chunk='dashboard-05-chunk-1'>
|
||||||
|
<CardHeader className='pb-2'>
|
||||||
|
<CardDescription>This Week</CardDescription>
|
||||||
|
<CardTitle className='text-4xl'>$1,329</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className='text-xs text-muted-foreground'>+25% from last week</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Progress value={25} aria-label='25% increase' />
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
<Card x-chunk='dashboard-05-chunk-2'>
|
||||||
|
<CardHeader className='pb-2'>
|
||||||
|
<CardDescription>This Month</CardDescription>
|
||||||
|
<CardTitle className='text-4xl'>$5,329</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className='text-xs text-muted-foreground'>+10% from last month</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Progress value={12} aria-label='12% increase' />
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<Tabs defaultValue='week'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value='week'>Week</TabsTrigger>
|
||||||
|
<TabsTrigger value='month'>Month</TabsTrigger>
|
||||||
|
<TabsTrigger value='year'>Year</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div className='flex items-center gap-2 ml-auto'>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant='outline' size='sm' className='gap-1 text-sm h-7'>
|
||||||
|
<ListFilter className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only sm:not-sr-only'>Filter</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuCheckboxItem checked>Fulfilled</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem>Declined</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem>Refunded</DropdownMenuCheckboxItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Button size='sm' variant='outline' className='gap-1 text-sm h-7'>
|
||||||
|
<File className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only sm:not-sr-only'>Export</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TabsContent value='week'>
|
||||||
|
<Card x-chunk='dashboard-05-chunk-3'>
|
||||||
|
<CardHeader className='px-7'>
|
||||||
|
<CardTitle>Orders</CardTitle>
|
||||||
|
<CardDescription>Recent orders from your store.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Customer</TableHead>
|
||||||
|
<TableHead className='hidden sm:table-cell'>Type</TableHead>
|
||||||
|
<TableHead className='hidden sm:table-cell'>Status</TableHead>
|
||||||
|
<TableHead className='hidden md:table-cell'>Date</TableHead>
|
||||||
|
<TableHead className='text-right'>Amount</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow className='bg-accent'>
|
||||||
|
<TableCell>
|
||||||
|
<div className='font-medium'>Liam Johnson</div>
|
||||||
|
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||||
|
liam@example.com
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<Badge className='text-xs' variant='secondary'>
|
||||||
|
Fulfilled
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
|
||||||
|
<TableCell className='text-right'>$250.00</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<div className='font-medium'>Olivia Smith</div>
|
||||||
|
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||||
|
olivia@example.com
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>Refund</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<Badge className='text-xs' variant='outline'>
|
||||||
|
Declined
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2023-06-24</TableCell>
|
||||||
|
<TableCell className='text-right'>$150.00</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<div className='font-medium'>Noah Williams</div>
|
||||||
|
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||||
|
noah@example.com
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>Subscription</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<Badge className='text-xs' variant='secondary'>
|
||||||
|
Fulfilled
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2023-06-25</TableCell>
|
||||||
|
<TableCell className='text-right'>$350.00</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<div className='font-medium'>Emma Brown</div>
|
||||||
|
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||||
|
emma@example.com
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<Badge className='text-xs' variant='secondary'>
|
||||||
|
Fulfilled
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2023-06-26</TableCell>
|
||||||
|
<TableCell className='text-right'>$450.00</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<div className='font-medium'>Liam Johnson</div>
|
||||||
|
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||||
|
liam@example.com
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<Badge className='text-xs' variant='secondary'>
|
||||||
|
Fulfilled
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
|
||||||
|
<TableCell className='text-right'>$250.00</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<div className='font-medium'>Liam Johnson</div>
|
||||||
|
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||||
|
liam@example.com
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<Badge className='text-xs' variant='secondary'>
|
||||||
|
Fulfilled
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
|
||||||
|
<TableCell className='text-right'>$250.00</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<div className='font-medium'>Olivia Smith</div>
|
||||||
|
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||||
|
olivia@example.com
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>Refund</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<Badge className='text-xs' variant='outline'>
|
||||||
|
Declined
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2023-06-24</TableCell>
|
||||||
|
<TableCell className='text-right'>$150.00</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<div className='font-medium'>Emma Brown</div>
|
||||||
|
<div className='hidden text-sm text-muted-foreground md:inline'>
|
||||||
|
emma@example.com
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<Badge className='text-xs' variant='secondary'>
|
||||||
|
Fulfilled
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2023-06-26</TableCell>
|
||||||
|
<TableCell className='text-right'>$450.00</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Card className='overflow-hidden' x-chunk='dashboard-05-chunk-4'>
|
||||||
|
<CardHeader className='flex flex-row items-start bg-muted/50'>
|
||||||
|
<div className='grid gap-0.5'>
|
||||||
|
<CardTitle className='flex items-center gap-2 text-lg group'>
|
||||||
|
Order Oe31b70H
|
||||||
|
<Button
|
||||||
|
size='icon'
|
||||||
|
variant='outline'
|
||||||
|
className='w-6 h-6 transition-opacity opacity-0 group-hover:opacity-100'
|
||||||
|
>
|
||||||
|
<Copy className='w-3 h-3' />
|
||||||
|
<span className='sr-only'>Copy Order ID</span>
|
||||||
|
</Button>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Date: November 23, 2023</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center gap-1 ml-auto'>
|
||||||
|
<Button size='sm' variant='outline' className='h-8 gap-1'>
|
||||||
|
<Truck className='h-3.5 w-3.5' />
|
||||||
|
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
|
||||||
|
Track Order
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button size='icon' variant='outline' className='w-8 h-8'>
|
||||||
|
<MoreVertical className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only'>More</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Export</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>Trash</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter>
|
<CardContent className='p-6 text-sm'>
|
||||||
<Button>Crear nueva cotización</Button>
|
<div className='grid gap-3'>
|
||||||
</CardFooter>
|
<div className='font-semibold'>Order Details</div>
|
||||||
</Card>
|
<ul className='grid gap-3'>
|
||||||
<Card x-chunk='dashboard-05-chunk-1'>
|
<li className='flex items-center justify-between'>
|
||||||
<CardHeader className='pb-2'>
|
<span className='text-muted-foreground'>
|
||||||
<CardDescription>This Week</CardDescription>
|
Glimmer Lamps x <span>2</span>
|
||||||
<CardTitle className='text-4xl'>$1,329</CardTitle>
|
</span>
|
||||||
</CardHeader>
|
<span>$250.00</span>
|
||||||
<CardContent>
|
</li>
|
||||||
<div className='text-xs text-muted-foreground'>+25% from last week</div>
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>
|
||||||
|
Aqua Filters x <span>1</span>
|
||||||
|
</span>
|
||||||
|
<span>$49.00</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<Separator className='my-2' />
|
||||||
|
<ul className='grid gap-3'>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>Subtotal</span>
|
||||||
|
<span>$299.00</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>Shipping</span>
|
||||||
|
<span>$5.00</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between'>
|
||||||
|
<span className='text-muted-foreground'>Tax</span>
|
||||||
|
<span>$25.00</span>
|
||||||
|
</li>
|
||||||
|
<li className='flex items-center justify-between font-semibold'>
|
||||||
|
<span className='text-muted-foreground'>Total</span>
|
||||||
|
<span>$329.00</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<Separator className='my-4' />
|
||||||
|
<div className='grid grid-cols-2 gap-4'>
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<div className='font-semibold'>Shipping Information</div>
|
||||||
|
<address className='grid gap-0.5 not-italic text-muted-foreground'>
|
||||||
|
<span>Liam Johnson</span>
|
||||||
|
<span>1234 Main St.</span>
|
||||||
|
<span>Anytown, CA 12345</span>
|
||||||
|
</address>
|
||||||
|
</div>
|
||||||
|
<div className='grid gap-3 auto-rows-max'>
|
||||||
|
<div className='font-semibold'>Billing Information</div>
|
||||||
|
<div className='text-muted-foreground'>Same as shipping address</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator className='my-4' />
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<div className='font-semibold'>Customer Information</div>
|
||||||
|
<dl className='grid gap-3'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<dt className='text-muted-foreground'>Customer</dt>
|
||||||
|
<dd>Liam Johnson</dd>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<dt className='text-muted-foreground'>Email</dt>
|
||||||
|
<dd>
|
||||||
|
<a href='mailto:'>liam@acme.com</a>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<dt className='text-muted-foreground'>Phone</dt>
|
||||||
|
<dd>
|
||||||
|
<a href='tel:'>+1 234 567 890</a>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<Separator className='my-4' />
|
||||||
|
<div className='grid gap-3'>
|
||||||
|
<div className='font-semibold'>Payment Information</div>
|
||||||
|
<dl className='grid gap-3'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<dt className='flex items-center gap-1 text-muted-foreground'>
|
||||||
|
<CreditCard className='w-4 h-4' />
|
||||||
|
Visa
|
||||||
|
</dt>
|
||||||
|
<dd>**** **** **** 4532</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter className='flex flex-row items-center px-6 py-3 border-t bg-muted/50'>
|
||||||
<Progress value={25} aria-label='25% increase' />
|
<div className='text-xs text-muted-foreground'>
|
||||||
</CardFooter>
|
Updated <time dateTime='2023-11-23'>November 23, 2023</time>
|
||||||
</Card>
|
</div>
|
||||||
<Card x-chunk='dashboard-05-chunk-2'>
|
<Pagination className='w-auto ml-auto mr-0'>
|
||||||
<CardHeader className='pb-2'>
|
<PaginationContent>
|
||||||
<CardDescription>This Month</CardDescription>
|
<PaginationItem>
|
||||||
<CardTitle className='text-4xl'>$5,329</CardTitle>
|
<Button size='icon' variant='outline' className='w-6 h-6'>
|
||||||
</CardHeader>
|
<ChevronLeft className='h-3.5 w-3.5' />
|
||||||
<CardContent>
|
<span className='sr-only'>Previous Order</span>
|
||||||
<div className='text-xs text-muted-foreground'>+10% from last month</div>
|
</Button>
|
||||||
</CardContent>
|
</PaginationItem>
|
||||||
<CardFooter>
|
<PaginationItem>
|
||||||
<Progress value={12} aria-label='12% increase' />
|
<Button size='icon' variant='outline' className='w-6 h-6'>
|
||||||
|
<ChevronRight className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only'>Next Order</span>
|
||||||
|
</Button>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<Tabs defaultValue='week'>
|
|
||||||
<div className='flex items-center'>
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value='week'>Week</TabsTrigger>
|
|
||||||
<TabsTrigger value='month'>Month</TabsTrigger>
|
|
||||||
<TabsTrigger value='year'>Year</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<div className='flex items-center gap-2 ml-auto'>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant='outline' size='sm' className='gap-1 text-sm h-7'>
|
|
||||||
<ListFilter className='h-3.5 w-3.5' />
|
|
||||||
<span className='sr-only sm:not-sr-only'>Filter</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align='end'>
|
|
||||||
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuCheckboxItem checked>Fulfilled</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuCheckboxItem>Declined</DropdownMenuCheckboxItem>
|
|
||||||
<DropdownMenuCheckboxItem>Refunded</DropdownMenuCheckboxItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<Button size='sm' variant='outline' className='gap-1 text-sm h-7'>
|
|
||||||
<File className='h-3.5 w-3.5' />
|
|
||||||
<span className='sr-only sm:not-sr-only'>Export</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TabsContent value='week'>
|
|
||||||
<Card x-chunk='dashboard-05-chunk-3'>
|
|
||||||
<CardHeader className='px-7'>
|
|
||||||
<CardTitle>Orders</CardTitle>
|
|
||||||
<CardDescription>Recent orders from your store.</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Customer</TableHead>
|
|
||||||
<TableHead className='hidden sm:table-cell'>Type</TableHead>
|
|
||||||
<TableHead className='hidden sm:table-cell'>Status</TableHead>
|
|
||||||
<TableHead className='hidden md:table-cell'>Date</TableHead>
|
|
||||||
<TableHead className='text-right'>Amount</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
<TableRow className='bg-accent'>
|
|
||||||
<TableCell>
|
|
||||||
<div className='font-medium'>Liam Johnson</div>
|
|
||||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
|
||||||
liam@example.com
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>
|
|
||||||
<Badge className='text-xs' variant='secondary'>
|
|
||||||
Fulfilled
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
|
|
||||||
<TableCell className='text-right'>$250.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<div className='font-medium'>Olivia Smith</div>
|
|
||||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
|
||||||
olivia@example.com
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>Refund</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>
|
|
||||||
<Badge className='text-xs' variant='outline'>
|
|
||||||
Declined
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden md:table-cell'>2023-06-24</TableCell>
|
|
||||||
<TableCell className='text-right'>$150.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<div className='font-medium'>Noah Williams</div>
|
|
||||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
|
||||||
noah@example.com
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>Subscription</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>
|
|
||||||
<Badge className='text-xs' variant='secondary'>
|
|
||||||
Fulfilled
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden md:table-cell'>2023-06-25</TableCell>
|
|
||||||
<TableCell className='text-right'>$350.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<div className='font-medium'>Emma Brown</div>
|
|
||||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
|
||||||
emma@example.com
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>
|
|
||||||
<Badge className='text-xs' variant='secondary'>
|
|
||||||
Fulfilled
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden md:table-cell'>2023-06-26</TableCell>
|
|
||||||
<TableCell className='text-right'>$450.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<div className='font-medium'>Liam Johnson</div>
|
|
||||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
|
||||||
liam@example.com
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>
|
|
||||||
<Badge className='text-xs' variant='secondary'>
|
|
||||||
Fulfilled
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
|
|
||||||
<TableCell className='text-right'>$250.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<div className='font-medium'>Liam Johnson</div>
|
|
||||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
|
||||||
liam@example.com
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>
|
|
||||||
<Badge className='text-xs' variant='secondary'>
|
|
||||||
Fulfilled
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden md:table-cell'>2023-06-23</TableCell>
|
|
||||||
<TableCell className='text-right'>$250.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<div className='font-medium'>Olivia Smith</div>
|
|
||||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
|
||||||
olivia@example.com
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>Refund</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>
|
|
||||||
<Badge className='text-xs' variant='outline'>
|
|
||||||
Declined
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden md:table-cell'>2023-06-24</TableCell>
|
|
||||||
<TableCell className='text-right'>$150.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>
|
|
||||||
<div className='font-medium'>Emma Brown</div>
|
|
||||||
<div className='hidden text-sm text-muted-foreground md:inline'>
|
|
||||||
emma@example.com
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>Sale</TableCell>
|
|
||||||
<TableCell className='hidden sm:table-cell'>
|
|
||||||
<Badge className='text-xs' variant='secondary'>
|
|
||||||
Fulfilled
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className='hidden md:table-cell'>2023-06-26</TableCell>
|
|
||||||
<TableCell className='text-right'>$450.00</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Card className='overflow-hidden' x-chunk='dashboard-05-chunk-4'>
|
|
||||||
<CardHeader className='flex flex-row items-start bg-muted/50'>
|
|
||||||
<div className='grid gap-0.5'>
|
|
||||||
<CardTitle className='flex items-center gap-2 text-lg group'>
|
|
||||||
Order Oe31b70H
|
|
||||||
<Button
|
|
||||||
size='icon'
|
|
||||||
variant='outline'
|
|
||||||
className='w-6 h-6 transition-opacity opacity-0 group-hover:opacity-100'
|
|
||||||
>
|
|
||||||
<Copy className='w-3 h-3' />
|
|
||||||
<span className='sr-only'>Copy Order ID</span>
|
|
||||||
</Button>
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>Date: November 23, 2023</CardDescription>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center gap-1 ml-auto'>
|
|
||||||
<Button size='sm' variant='outline' className='h-8 gap-1'>
|
|
||||||
<Truck className='h-3.5 w-3.5' />
|
|
||||||
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
|
|
||||||
Track Order
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button size='icon' variant='outline' className='w-8 h-8'>
|
|
||||||
<MoreVertical className='h-3.5 w-3.5' />
|
|
||||||
<span className='sr-only'>More</span>
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align='end'>
|
|
||||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>Export</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem>Trash</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className='p-6 text-sm'>
|
|
||||||
<div className='grid gap-3'>
|
|
||||||
<div className='font-semibold'>Order Details</div>
|
|
||||||
<ul className='grid gap-3'>
|
|
||||||
<li className='flex items-center justify-between'>
|
|
||||||
<span className='text-muted-foreground'>
|
|
||||||
Glimmer Lamps x <span>2</span>
|
|
||||||
</span>
|
|
||||||
<span>$250.00</span>
|
|
||||||
</li>
|
|
||||||
<li className='flex items-center justify-between'>
|
|
||||||
<span className='text-muted-foreground'>
|
|
||||||
Aqua Filters x <span>1</span>
|
|
||||||
</span>
|
|
||||||
<span>$49.00</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Separator className='my-2' />
|
|
||||||
<ul className='grid gap-3'>
|
|
||||||
<li className='flex items-center justify-between'>
|
|
||||||
<span className='text-muted-foreground'>Subtotal</span>
|
|
||||||
<span>$299.00</span>
|
|
||||||
</li>
|
|
||||||
<li className='flex items-center justify-between'>
|
|
||||||
<span className='text-muted-foreground'>Shipping</span>
|
|
||||||
<span>$5.00</span>
|
|
||||||
</li>
|
|
||||||
<li className='flex items-center justify-between'>
|
|
||||||
<span className='text-muted-foreground'>Tax</span>
|
|
||||||
<span>$25.00</span>
|
|
||||||
</li>
|
|
||||||
<li className='flex items-center justify-between font-semibold'>
|
|
||||||
<span className='text-muted-foreground'>Total</span>
|
|
||||||
<span>$329.00</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<Separator className='my-4' />
|
|
||||||
<div className='grid grid-cols-2 gap-4'>
|
|
||||||
<div className='grid gap-3'>
|
|
||||||
<div className='font-semibold'>Shipping Information</div>
|
|
||||||
<address className='grid gap-0.5 not-italic text-muted-foreground'>
|
|
||||||
<span>Liam Johnson</span>
|
|
||||||
<span>1234 Main St.</span>
|
|
||||||
<span>Anytown, CA 12345</span>
|
|
||||||
</address>
|
|
||||||
</div>
|
|
||||||
<div className='grid gap-3 auto-rows-max'>
|
|
||||||
<div className='font-semibold'>Billing Information</div>
|
|
||||||
<div className='text-muted-foreground'>Same as shipping address</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator className='my-4' />
|
|
||||||
<div className='grid gap-3'>
|
|
||||||
<div className='font-semibold'>Customer Information</div>
|
|
||||||
<dl className='grid gap-3'>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<dt className='text-muted-foreground'>Customer</dt>
|
|
||||||
<dd>Liam Johnson</dd>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<dt className='text-muted-foreground'>Email</dt>
|
|
||||||
<dd>
|
|
||||||
<a href='mailto:'>liam@acme.com</a>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<dt className='text-muted-foreground'>Phone</dt>
|
|
||||||
<dd>
|
|
||||||
<a href='tel:'>+1 234 567 890</a>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
<Separator className='my-4' />
|
|
||||||
<div className='grid gap-3'>
|
|
||||||
<div className='font-semibold'>Payment Information</div>
|
|
||||||
<dl className='grid gap-3'>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<dt className='flex items-center gap-1 text-muted-foreground'>
|
|
||||||
<CreditCard className='w-4 h-4' />
|
|
||||||
Visa
|
|
||||||
</dt>
|
|
||||||
<dd>**** **** **** 4532</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className='flex flex-row items-center px-6 py-3 border-t bg-muted/50'>
|
|
||||||
<div className='text-xs text-muted-foreground'>
|
|
||||||
Updated <time dateTime='2023-11-23'>November 23, 2023</time>
|
|
||||||
</div>
|
|
||||||
<Pagination className='w-auto ml-auto mr-0'>
|
|
||||||
<PaginationContent>
|
|
||||||
<PaginationItem>
|
|
||||||
<Button size='icon' variant='outline' className='w-6 h-6'>
|
|
||||||
<ChevronLeft className='h-3.5 w-3.5' />
|
|
||||||
<span className='sr-only'>Previous Order</span>
|
|
||||||
</Button>
|
|
||||||
</PaginationItem>
|
|
||||||
<PaginationItem>
|
|
||||||
<Button size='icon' variant='outline' className='w-6 h-6'>
|
|
||||||
<ChevronRight className='h-3.5 w-3.5' />
|
|
||||||
<span className='sr-only'>Next Order</span>
|
|
||||||
</Button>
|
|
||||||
</PaginationItem>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
|
export * from "./layout";
|
||||||
export * from "./list";
|
export * from "./list";
|
||||||
|
|||||||
11
client/src/app/dealers/layout.tsx
Normal file
11
client/src/app/dealers/layout.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Layout, LayoutContent, LayoutHeader } from "@/components";
|
||||||
|
import { PropsWithChildren } from "react";
|
||||||
|
|
||||||
|
export const DealerLayout = ({ children }: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<LayoutHeader />
|
||||||
|
<LayoutContent>{children}</LayoutContent>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 = () => {
|
export const DealersList = () => {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<LayoutHeader />
|
<Tabs defaultValue='all'>
|
||||||
<LayoutContent>
|
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<h1 className='text-lg font-semibold md:text-2xl'>Dealers</h1>
|
<TabsList>
|
||||||
|
<TabsTrigger value='all'>All</TabsTrigger>
|
||||||
|
<TabsTrigger value='active'>Active</TabsTrigger>
|
||||||
|
<TabsTrigger value='draft'>Draft</TabsTrigger>
|
||||||
|
<TabsTrigger value='archived' className='hidden sm:flex'>
|
||||||
|
Archived
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div className='flex items-center gap-2 ml-auto'>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant='outline' size='sm' className='h-8 gap-1'>
|
||||||
|
<ListFilter className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Filter</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuCheckboxItem checked>Active</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem>Draft</DropdownMenuCheckboxItem>
|
||||||
|
<DropdownMenuCheckboxItem>Archived</DropdownMenuCheckboxItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Button size='sm' variant='outline' className='h-8 gap-1'>
|
||||||
|
<File className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Export</span>
|
||||||
|
</Button>
|
||||||
|
<Button size='sm' className='h-8 gap-1'>
|
||||||
|
<PlusCircle className='h-3.5 w-3.5' />
|
||||||
|
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Add Product</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</LayoutContent>
|
<TabsContent value='all'>
|
||||||
</Layout>
|
<Card x-chunk='dashboard-06-chunk-0'>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>
|
||||||
|
<Trans i18nKey='catalog.title' />
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Manage your products and view their sales performance.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className='hidden w-[100px] sm:table-cell'>
|
||||||
|
<span className='sr-only'>Image</span>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead className='hidden md:table-cell'>Price</TableHead>
|
||||||
|
<TableHead className='hidden md:table-cell'>Total Sales</TableHead>
|
||||||
|
<TableHead className='hidden md:table-cell'>Created at</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
<span className='sr-only'>Actions</span>
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<img
|
||||||
|
alt='Product image'
|
||||||
|
className='object-cover rounded-md aspect-square'
|
||||||
|
height='64'
|
||||||
|
src='/placeholder.svg'
|
||||||
|
width='64'
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='font-medium'>Laser Lemonade Machine</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant='outline'>Draft</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>$499.99</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>25</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2023-07-12 10:42 AM</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
||||||
|
<MoreHorizontal className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>Toggle menu</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<img
|
||||||
|
alt='Product image'
|
||||||
|
className='object-cover rounded-md aspect-square'
|
||||||
|
height='64'
|
||||||
|
src='/placeholder.svg'
|
||||||
|
width='64'
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='font-medium'>Hypernova Headphones</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant='outline'>Active</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>$129.99</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>100</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2023-10-18 03:21 PM</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
||||||
|
<MoreHorizontal className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>Toggle menu</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<img
|
||||||
|
alt='Product image'
|
||||||
|
className='object-cover rounded-md aspect-square'
|
||||||
|
height='64'
|
||||||
|
src='/placeholder.svg'
|
||||||
|
width='64'
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='font-medium'>AeroGlow Desk Lamp</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant='outline'>Active</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>$39.99</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>50</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2023-11-29 08:15 AM</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
||||||
|
<MoreHorizontal className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>Toggle menu</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<img
|
||||||
|
alt='Product image'
|
||||||
|
className='object-cover rounded-md aspect-square'
|
||||||
|
height='64'
|
||||||
|
src='/placeholder.svg'
|
||||||
|
width='64'
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='font-medium'>TechTonic Energy Drink</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant='secondary'>Draft</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>$2.99</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>0</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2023-12-25 11:59 PM</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
||||||
|
<MoreHorizontal className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>Toggle menu</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<img
|
||||||
|
alt='Product image'
|
||||||
|
className='object-cover rounded-md aspect-square'
|
||||||
|
height='64'
|
||||||
|
src='/placeholder.svg'
|
||||||
|
width='64'
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='font-medium'>Gamer Gear Pro Controller</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant='outline'>Active</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>$59.99</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>75</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2024-01-01 12:00 AM</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
||||||
|
<MoreHorizontal className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>Toggle menu</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className='hidden sm:table-cell'>
|
||||||
|
<img
|
||||||
|
alt='Product image'
|
||||||
|
className='object-cover rounded-md aspect-square'
|
||||||
|
height='64'
|
||||||
|
src='/placeholder.svg'
|
||||||
|
width='64'
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='font-medium'>Luminous VR Headset</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant='outline'>Active</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>$199.99</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>30</TableCell>
|
||||||
|
<TableCell className='hidden md:table-cell'>2024-02-14 02:14 PM</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button aria-haspopup='true' size='icon' variant='ghost'>
|
||||||
|
<MoreHorizontal className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>Toggle menu</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align='end'>
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<div className='text-xs text-muted-foreground'>
|
||||||
|
Showing <strong>1-10</strong> of <strong>32</strong> products
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
14
client/src/components/ButtonGroup/ButtonGroup.tsx
Normal file
14
client/src/components/ButtonGroup/ButtonGroup.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const ButtonGroup = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("ml-auto flex items-center gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ButtonGroup.displayName = "ButtonGroup";
|
||||||
1
client/src/components/ButtonGroup/index.ts
Normal file
1
client/src/components/ButtonGroup/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./ButtonGroup";
|
||||||
31
client/src/components/CustomButtons/BackHistoryButton.tsx
Normal file
31
client/src/components/CustomButtons/BackHistoryButton.tsx
Normal file
@ -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 (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
url ? navigate(url) : navigate(-1);
|
||||||
|
}}
|
||||||
|
size={size}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
<span className={size === "icon" ? "sr-only" : "ml-2"}>{label}</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
client/src/components/CustomButtons/index.ts
Normal file
1
client/src/components/CustomButtons/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./BackHistoryButton";
|
||||||
97
client/src/components/DataTable/DataTable.tsx
Normal file
97
client/src/components/DataTable/DataTable.tsx
Normal file
@ -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<TData, TValue> = ColumnDef<TData, TValue>;
|
||||||
|
|
||||||
|
export type DataTablePaginationOptionsProps<TData> = Pick<
|
||||||
|
DataTablePaginationProps<TData>,
|
||||||
|
"visible"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DataTableProps<TData> = {
|
||||||
|
table: ReactTable<TData>;
|
||||||
|
caption?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
|
||||||
|
paginationOptions?: DataTablePaginationOptionsProps<TData>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DataTable<TData>({ table, caption, paginationOptions }: DataTableProps<TData>) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DataTableToolbar table={table} />
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<DataTablePagination
|
||||||
|
className='flex-1'
|
||||||
|
visible={paginationOptions?.visible}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className='pt-6'>
|
||||||
|
<Table>
|
||||||
|
{typeof caption !== "undefined" && <TableCaption>{caption}</TableCaption>}
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead
|
||||||
|
key={header.id}
|
||||||
|
colSpan={header.colSpan}
|
||||||
|
style={{ width: header.getSize() }}
|
||||||
|
>
|
||||||
|
<DataTableColumnHeader table={table} header={header} />
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={table.getAllColumns.length} className='h-24 text-center'>
|
||||||
|
No hay datos para mostrar
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<DataTablePagination
|
||||||
|
className='flex-1'
|
||||||
|
visible={paginationOptions?.visible}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
123
client/src/components/DataTable/DataTableColumnHeader.tsx
Normal file
123
client/src/components/DataTable/DataTableColumnHeader.tsx
Normal file
@ -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<TData, TValue>
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
table: Table<TData>;
|
||||||
|
header: Header<TData, TValue>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTableColumnHeader<TData, TValue>({
|
||||||
|
table,
|
||||||
|
header,
|
||||||
|
className,
|
||||||
|
}: DataTableColumnHeaderProps<TData, TValue>) {
|
||||||
|
if (!header.column.getCanSort()) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:bg-accent font-semiboldw text-muted-foreground uppercase",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</div>
|
||||||
|
{header.column.getCanResize() && (
|
||||||
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
className={cn(
|
||||||
|
"absolute top-0 h-full w-[5px] bg-black/10 cursor-col-resize",
|
||||||
|
table.options.columnResizeDirection,
|
||||||
|
header.column.getIsResizing() ? "bg-primary opacity-100" : "",
|
||||||
|
)}
|
||||||
|
{...{
|
||||||
|
onDoubleClick: () => 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 (
|
||||||
|
<div className={cn("flex items-center space-x-2", className)}>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
aria-label={
|
||||||
|
header.column.getIsSorted() === "desc"
|
||||||
|
? "En orden descendente. Click para ordenar ascendentemente."
|
||||||
|
: header.column.getIsSorted() === "asc"
|
||||||
|
? "En order ascendente. Click para ordenar descendentemente."
|
||||||
|
: "Sin orden. Click para ordenar ascendentemente."
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="-ml-3 h-8 data-[state=open]:bg-accent font-bold text-muted-foreground"
|
||||||
|
>
|
||||||
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
{header.column.getIsSorted() === "desc" ? (
|
||||||
|
<ArrowDownIcon className="w-4 h-4 ml-2" />
|
||||||
|
) : header.column.getIsSorted() === "asc" ? (
|
||||||
|
<ArrowUpIcon className="w-4 h-4 ml-2" />
|
||||||
|
) : (
|
||||||
|
<ArrowDownUpIcon className="w-4 h-4 ml-2 text-muted-foreground/30" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<DropdownMenuItem onClick={() => header.column.toggleSorting(false)}>
|
||||||
|
<ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
||||||
|
Ascendente
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => header.column.toggleSorting(true)}>
|
||||||
|
<ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
||||||
|
Descendente
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{header.column.getCanHide() && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => header.column.toggleVisibility(false)}
|
||||||
|
>
|
||||||
|
<EyeOffIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
||||||
|
Ocultar
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
149
client/src/components/DataTable/DataTableFacetedFilter.tsx
Normal file
149
client/src/components/DataTable/DataTableFacetedFilter.tsx
Normal file
@ -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<TData, TValue> {
|
||||||
|
column?: Column<TData, TValue>
|
||||||
|
title?: string
|
||||||
|
options: {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
icon?: React.ComponentType<{ className?: string }>
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTableFacetedFilter<TData, TValue>({
|
||||||
|
column,
|
||||||
|
title,
|
||||||
|
options,
|
||||||
|
}: DataTableFacetedFilterProps<TData, TValue>) {
|
||||||
|
const facets = column?.getFacetedUniqueValues()
|
||||||
|
const selectedValues = new Set(column?.getFilterValue() as string[])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
||||||
|
<PlusCircleIcon className="w-4 h-4 mr-2" />
|
||||||
|
{title}
|
||||||
|
{selectedValues?.size > 0 && (
|
||||||
|
<>
|
||||||
|
<Separator orientation="vertical" className="h-4 mx-2" />
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="px-1 font-normal rounded-sm lg:hidden"
|
||||||
|
>
|
||||||
|
{selectedValues.size}
|
||||||
|
</Badge>
|
||||||
|
<div className="hidden space-x-1 lg:flex">
|
||||||
|
{selectedValues.size > 2 ? (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="px-1 font-normal rounded-sm"
|
||||||
|
>
|
||||||
|
{selectedValues.size} selected
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
options
|
||||||
|
.filter((option) => selectedValues.has(option.value))
|
||||||
|
.map((option) => (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
key={option.value}
|
||||||
|
className="px-1 font-normal rounded-sm"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder={title} />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{options.map((option) => {
|
||||||
|
const isSelected = selectedValues.has(option.value)
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
onSelect={() => {
|
||||||
|
if (isSelected) {
|
||||||
|
selectedValues.delete(option.value)
|
||||||
|
} else {
|
||||||
|
selectedValues.add(option.value)
|
||||||
|
}
|
||||||
|
const filterValues = Array.from(selectedValues)
|
||||||
|
column?.setFilterValue(
|
||||||
|
filterValues.length ? filterValues : undefined
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||||
|
isSelected
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "opacity-50 [&_svg]:invisible"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon className={cn("h-4 w-4")} />
|
||||||
|
</div>
|
||||||
|
{option.icon && (
|
||||||
|
<option.icon className="w-4 h-4 mr-2 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<span>{option.label}</span>
|
||||||
|
{facets?.get(option.value) && (
|
||||||
|
<span className="flex items-center justify-center w-4 h-4 ml-auto font-mono text-xs">
|
||||||
|
{facets.get(option.value)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</CommandItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
{selectedValues.size > 0 && (
|
||||||
|
<>
|
||||||
|
<CommandSeparator />
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => column?.setFilterValue(undefined)}
|
||||||
|
className="justify-center text-center"
|
||||||
|
>
|
||||||
|
Clear filters
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
102
client/src/components/DataTable/DataTablePagination.tsx
Normal file
102
client/src/components/DataTable/DataTablePagination.tsx
Normal file
@ -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<TData> = {
|
||||||
|
table: Table<TData>;
|
||||||
|
className?: string;
|
||||||
|
visible?: boolean | "auto";
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DataTablePagination<TData>({
|
||||||
|
table,
|
||||||
|
className,
|
||||||
|
visible = "auto",
|
||||||
|
}: DataTablePaginationProps<TData>) {
|
||||||
|
const isVisible = useMemo(() => visible === true, [visible]);
|
||||||
|
const isAuto = useMemo(() => visible === "auto", [visible]);
|
||||||
|
|
||||||
|
if (!isVisible || (isAuto && table.getPageCount() < 1)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center justify-between px-2", className)}>
|
||||||
|
<div className='flex-1 text-base text-muted-foreground'>
|
||||||
|
{table.getFilteredSelectedRowModel().rows.length} de{" "}
|
||||||
|
{table.getFilteredRowModel().rows.length} filas(s) seleccionadas.
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center space-x-6 lg:space-x-8'>
|
||||||
|
<div className='flex items-center space-x-2'>
|
||||||
|
<p className='text-base font-medium'>Filas por página</p>
|
||||||
|
<Select
|
||||||
|
value={`${table.getState().pagination.pageSize}`}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
table.setPageSize(Number(value));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className='h-8 w-[70px]'>
|
||||||
|
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent side='top'>
|
||||||
|
{DEFAULT_PAGE_SIZES.map((pageSize) => (
|
||||||
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||||
|
{pageSize}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className='flex w-[100px] items-center justify-center text-base font-medium'>
|
||||||
|
Página {table.getState().pagination.pageIndex + 1} de {table.getPageCount()}
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center space-x-2'>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
className='hidden w-8 h-8 p-0 lg:flex'
|
||||||
|
onClick={() => table.setPageIndex(INITIAL_PAGE_INDEX)}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<span className='sr-only'>Ir a la primera página</span>
|
||||||
|
<ChevronsLeftIcon className='w-4 h-4' />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
className='w-8 h-8 p-0'
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<span className='sr-only'>Ir a la página anterior</span>
|
||||||
|
<ChevronLeftIcon className='w-4 h-4' />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
className='w-8 h-8 p-0'
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<span className='sr-only'>Ir a la página siguiente</span>
|
||||||
|
<ChevronRightIcon className='w-4 h-4' />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
className='hidden w-8 h-8 p-0 lg:flex'
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() + 1)}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<span className='sr-only'>Ir a la última página</span>
|
||||||
|
<ChevronsRightIcon className='w-4 h-4' />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
75
client/src/components/DataTable/DataTableRowActions.tsx
Normal file
75
client/src/components/DataTable/DataTableRowActions.tsx
Normal file
@ -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<TData, TValue> = CellContext<TData, TValue>;
|
||||||
|
|
||||||
|
export type DataTablaRowActionFunction<TData, TValue> = (
|
||||||
|
props: DataTableRowActionContext<TData, TValue>,
|
||||||
|
) => DataTableRowActionDefinition<TData, TValue>[];
|
||||||
|
|
||||||
|
export type DataTableRowActionDefinition<TData, TValue> = {
|
||||||
|
label: string | "-";
|
||||||
|
shortcut?: string;
|
||||||
|
onClick?: (
|
||||||
|
props: DataTableRowActionContext<TData, TValue>,
|
||||||
|
e: React.BaseSyntheticEvent,
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DataTableRowActionsProps<TData, TValue> = {
|
||||||
|
props: DataTableRowActionContext<TData, TValue>;
|
||||||
|
actions?: DataTablaRowActionFunction<TData, TValue>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DataTableRowActions<TData, TValue>({
|
||||||
|
actions,
|
||||||
|
...props
|
||||||
|
}: DataTableRowActionsProps<TData, TValue>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
aria-haspopup="true"
|
||||||
|
size="icon"
|
||||||
|
variant="link"
|
||||||
|
className="w-4 h-4 translate-y-[2px]"
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon className="w-4 h-4" />
|
||||||
|
<span className="sr-only">Abrir menú</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Acciones</DropdownMenuLabel>
|
||||||
|
{actions &&
|
||||||
|
actions(props).map((action, index) =>
|
||||||
|
action.label === "-" ? (
|
||||||
|
<DropdownMenuSeparator key={index} />
|
||||||
|
) : (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={index}
|
||||||
|
onClick={(event) =>
|
||||||
|
action.onClick ? action.onClick(props, event) : null
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
<DropdownMenuShortcut>{action.shortcut}</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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 (
|
||||||
|
<Button
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
size="icon"
|
||||||
|
variant="link"
|
||||||
|
className={cn(
|
||||||
|
isDragging ? "cursor-grabbing" : "cursor-grab",
|
||||||
|
"w-4 h-4 translate-y-[2px]"
|
||||||
|
)}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<GripVerticalIcon className="w-4 h-4" />
|
||||||
|
<span className="sr-only">Mover fila</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
36
client/src/components/DataTable/DataTableSelectionColumn.tsx
Normal file
36
client/src/components/DataTable/DataTableSelectionColumn.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Checkbox } from "@/ui";
|
||||||
|
import { DataTableColumnProps } from "./DataTable";
|
||||||
|
|
||||||
|
export function getDataTableSelectionColumn<
|
||||||
|
TData,
|
||||||
|
TError,
|
||||||
|
>(): DataTableColumnProps<TData, TError> {
|
||||||
|
return {
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => (
|
||||||
|
<Checkbox
|
||||||
|
id="select-all"
|
||||||
|
checked={
|
||||||
|
table.getIsAllPageRowsSelected() ||
|
||||||
|
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||||
|
aria-label="Seleccionar todo"
|
||||||
|
className="translate-y-[2px]"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row, table }) => (
|
||||||
|
<Checkbox
|
||||||
|
id={`select-row-${row.id}`}
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
row.toggleSelected(!!value);
|
||||||
|
}}
|
||||||
|
aria-label="Seleccionar file"
|
||||||
|
className="translate-y-[2px]"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
149
client/src/components/DataTable/DataTableSkeleton.tsx
Normal file
149
client/src/components/DataTable/DataTableSkeleton.tsx
Normal file
@ -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 (
|
||||||
|
<div className="w-full space-y-3 overflow-auto">
|
||||||
|
<div className="flex items-center justify-between w-full p-1 space-x-2 overflow-auto">
|
||||||
|
<div className="flex items-center flex-1 space-x-2">
|
||||||
|
{searchableColumnCount > 0
|
||||||
|
? Array.from({ length: searchableColumnCount }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="w-40 h-7 lg:w-60" />
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
{filterableColumnCount > 0
|
||||||
|
? Array.from({ length: filterableColumnCount }).map((_, i) => (
|
||||||
|
<Skeleton key={i} className="h-7 w-[4.5rem] border-dashed" />
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
{showViewOptions ? (
|
||||||
|
<Skeleton className="ml-auto hidden h-7 w-[4.5rem] lg:flex" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-md">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{Array.from({ length: 1 }).map((_, i) => (
|
||||||
|
<TableRow key={i} className="hover:bg-transparent">
|
||||||
|
{Array.from({ length: columnCount }).map((_, j) => (
|
||||||
|
<TableHead
|
||||||
|
key={j}
|
||||||
|
style={{
|
||||||
|
width: cellWidths[j],
|
||||||
|
minWidth: shrinkZero ? cellWidths[j] : "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Skeleton className="w-full h-6" />
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{Array.from({ length: rowCount }).map((_, i) => (
|
||||||
|
<TableRow key={i} className="hover:bg-transparent">
|
||||||
|
{Array.from({ length: columnCount }).map((_, j) => (
|
||||||
|
<TableCell
|
||||||
|
key={j}
|
||||||
|
style={{
|
||||||
|
width: cellWidths[j],
|
||||||
|
minWidth: shrinkZero ? cellWidths[j] : "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Skeleton className="w-full h-6" />
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col-reverse items-center justify-between w-full gap-4 px-2 py-1 overflow-auto sm:flex-row sm:gap-8">
|
||||||
|
<Skeleton className="w-40 h-8" />
|
||||||
|
<div className="flex flex-col-reverse items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Skeleton className="w-24 h-8" />
|
||||||
|
<Skeleton className="h-8 w-[4.5rem]" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center text-sm font-medium">
|
||||||
|
<Skeleton className="w-20 h-8" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Skeleton className="hidden size-8 lg:block" />
|
||||||
|
<Skeleton className="size-8" />
|
||||||
|
<Skeleton className="size-8" />
|
||||||
|
<Skeleton className="hidden size-8 lg:block" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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<TData> {
|
||||||
|
table: Table<TData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTableColumnOptions<TData>({
|
||||||
|
table,
|
||||||
|
}: DataTableColumnOptionsProps<TData>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="hidden h-8 ml-auto lg:flex"
|
||||||
|
>
|
||||||
|
<Settings2Icon className="w-4 h-4 mr-2" />
|
||||||
|
Columnas
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-[150px]">
|
||||||
|
<DropdownMenuLabel>Columnas</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{table
|
||||||
|
.getAllColumns()
|
||||||
|
.filter(
|
||||||
|
(column) =>
|
||||||
|
typeof column.accessorFn !== "undefined" && column.getCanHide()
|
||||||
|
)
|
||||||
|
.map((column) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={column.id}
|
||||||
|
className="capitalize"
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||||
|
>
|
||||||
|
<>{column.columnDef.header}</>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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<TData>
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
table: Table<TData>;
|
||||||
|
filterFields?: DataTableFilterField<TData>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTableToolbar<TData>({
|
||||||
|
table,
|
||||||
|
filterFields = [],
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: DataTableToolbarProps<TData>) {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-center justify-between space-x-2 overflow-auto p-1",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="flex items-center flex-1 space-x-2">
|
||||||
|
{searchableColumns.length > 0 &&
|
||||||
|
searchableColumns.map(
|
||||||
|
(column) =>
|
||||||
|
table.getColumn(column.value ? String(column.value) : "") && (
|
||||||
|
<Input
|
||||||
|
key={String(column.value)}
|
||||||
|
placeholder={column.placeholder}
|
||||||
|
value={
|
||||||
|
(table
|
||||||
|
.getColumn(String(column.value))
|
||||||
|
?.getFilterValue() as string) ?? ""
|
||||||
|
}
|
||||||
|
onChange={(event) =>
|
||||||
|
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) : "") && (
|
||||||
|
<DataTableFacetedFilter
|
||||||
|
key={String(column.value)}
|
||||||
|
column={table.getColumn(
|
||||||
|
column.value ? String(column.value) : "",
|
||||||
|
)}
|
||||||
|
title={column.label}
|
||||||
|
options={column.options ?? []}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
{isFiltered && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => table.resetColumnFilters()}
|
||||||
|
className="h-8 px-2 lg:px-3"
|
||||||
|
>
|
||||||
|
Reset filters
|
||||||
|
<CrossIcon className="w-4 h-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{children}
|
||||||
|
{table.options.enableHiding && <DataTableColumnOptions table={table} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center flex-1 space-x-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter tasks..."
|
||||||
|
value={(table.getColumn("customer")?.getFilterValue() as string) ?? ""}
|
||||||
|
onChange={(event) =>
|
||||||
|
table.getColumn("customer")?.setFilterValue(event.target.value)
|
||||||
|
}
|
||||||
|
className="h-8 w-[150px] lg:w-[250px]"
|
||||||
|
/>
|
||||||
|
{table.getColumn("status") && (
|
||||||
|
<DataTableFacetedFilter
|
||||||
|
column={table.getColumn("status")}
|
||||||
|
title="Status"
|
||||||
|
options={statuses}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{table.getColumn("priority") && (
|
||||||
|
<DataTableFacetedFilter
|
||||||
|
column={table.getColumn("priority")}
|
||||||
|
title="Priority"
|
||||||
|
options={priorities}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isFiltered && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => table.resetColumnFilters()}
|
||||||
|
className="h-8 px-2 lg:px-3"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
<CrossIcon className="w-4 h-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<DataTableViewOptions table={table} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./DataTableToolbar";
|
||||||
6
client/src/components/DataTable/index.ts
Normal file
6
client/src/components/DataTable/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from "./DataTable";
|
||||||
|
export * from "./DataTableColumnHeader";
|
||||||
|
export * from "./DataTableRowActions";
|
||||||
|
export * from "./DataTableRowDragHandleCell";
|
||||||
|
export * from "./DataTableSelectionColumn";
|
||||||
|
export * from "./DataTableSkeleton";
|
||||||
42
client/src/components/EmptyState/SimpleEmptyState.tsx
Normal file
42
client/src/components/EmptyState/SimpleEmptyState.tsx
Normal file
@ -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 (
|
||||||
|
<div className="text-center">
|
||||||
|
<svg
|
||||||
|
className="w-12 h-12 mx-auto text-slate-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
vectorEffect="non-scaling-stroke"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<h3 className="mt-2 text-sm font-semibold text-slate-900">{title}</h3>
|
||||||
|
<p className="mt-1 text-sm text-slate-500">{subtitle}</p>
|
||||||
|
|
||||||
|
<div className="items-center mt-6">
|
||||||
|
{actions && <>{actions}</>}
|
||||||
|
{!actions && (
|
||||||
|
<Button className="my-4" onClick={onButtonClick}>
|
||||||
|
<PlusIcon />
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
client/src/components/EmptyState/index.ts
Normal file
1
client/src/components/EmptyState/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./SimpleEmptyState";
|
||||||
20
client/src/components/ErrorOverlay/ErrorOverlay.module.css
Normal file
20
client/src/components/ErrorOverlay/ErrorOverlay.module.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
client/src/components/ErrorOverlay/ErrorOverlay.tsx
Normal file
53
client/src/components/ErrorOverlay/ErrorOverlay.tsx
Normal file
@ -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 (
|
||||||
|
<div className='grid h-screen place-items-center '>
|
||||||
|
<div className='text-center'>
|
||||||
|
<h2 className='mt-2 text-xl font-semibold text-center text-slate-900'>{title}</h2>
|
||||||
|
<p className='mt-1 font-medium text-slate-500'>
|
||||||
|
{subtitle || errorMessage}
|
||||||
|
<br />
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
<div className='mt-6 space-x-8 md:ml-4'>
|
||||||
|
<BackHistoryButton />
|
||||||
|
<Button onClick={() => navigate("/")}>
|
||||||
|
<HomeIcon className='w-4 h-4 mr-2' /> Ir al inicio
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ErrorOverlay.displayName = "ErrorOverlay";
|
||||||
1
client/src/components/ErrorOverlay/index.ts
Normal file
1
client/src/components/ErrorOverlay/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './ErrorOverlay';
|
||||||
30
client/src/components/SorteableDataTable/AddNewRowButton.tsx
Normal file
30
client/src/components/SorteableDataTable/AddNewRowButton.tsx
Normal file
@ -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 => (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
"w-full gap-1 border-dashed text-muted-foreground border-muted-foreground/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<PlusCircleIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
|
||||||
|
{label && <>{label}</>}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
AddNewRowButton.displayName = "AddNewRowButton";
|
||||||
463
client/src/components/SorteableDataTable/SortableDataTable.tsx
Normal file
463
client/src/components/SorteableDataTable/SortableDataTable.tsx
Normal file
@ -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<TData extends RowData> {
|
||||||
|
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<unknown, unknown>[];
|
||||||
|
data: Record<"id", string>[];
|
||||||
|
actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "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<ColumnDef<unknown>> = {
|
||||||
|
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 (
|
||||||
|
<input
|
||||||
|
value={value as string}
|
||||||
|
onChange={(e) => 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<RowSelectionState>({});
|
||||||
|
const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||||
|
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<unknown>[]) {
|
||||||
|
if (!activeId) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.filter((idOrRow) => {
|
||||||
|
const id = typeof idOrRow === "string" ? idOrRow : idOrRow.id;
|
||||||
|
return id === activeId || !table.getSelectedRowModel().rowsById[id];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
measuring={measuringConfig}
|
||||||
|
sensors={sensors}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragCancel={handleDragCancel}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
>
|
||||||
|
<SortableDataTableToolbar table={table} />
|
||||||
|
<Table className='table-auto'>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id} className='hover:bg-transparent'>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id} className='px-1'>
|
||||||
|
{header.isPlaceholder ? null : (
|
||||||
|
<DataTableColumnHeader table={table} header={header} />
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<SortableContext
|
||||||
|
items={filterItems(sorteableRowIds)}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
{filterItems(table.getRowModel().rows).map((row) => (
|
||||||
|
<SortableTableRow key={row.id} id={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
className='p-1 align-top'
|
||||||
|
key={cell.id}
|
||||||
|
style={{ width: cell.column.getSize() }}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</SortableTableRow>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter className='bg-default'>
|
||||||
|
<TableRow className='hover:bg-default'>
|
||||||
|
<TableCell colSpan={6} className='py-6'>
|
||||||
|
<AddNewRowButton onClick={hadleNewItem} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{createPortal(
|
||||||
|
<DragOverlay dropAnimation={dropAnimationConfig} className={"z-40 opacity-100"}>
|
||||||
|
{activeId && (
|
||||||
|
<div className='relative flex flex-wrap'>
|
||||||
|
{table.getSelectedRowModel().rows.length ? (
|
||||||
|
<Badge
|
||||||
|
variant='destructive'
|
||||||
|
className='absolute z-50 flex items-center justify-center w-2 h-2 p-3 rounded-full top left -left-2 -top-2'
|
||||||
|
>
|
||||||
|
{table.getSelectedRowModel().rows.length}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
<div className='absolute z-40 bg-white border rounded shadow opacity-100 top left hover:bg-white border-muted-foreground/50'>
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows.map(
|
||||||
|
(row) =>
|
||||||
|
row.id === activeId && (
|
||||||
|
<TableRow key={row.id} id={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
className='p-1 align-top'
|
||||||
|
key={cell.id}
|
||||||
|
style={{ width: cell.column.getSize() }}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{table.getSelectedRowModel().rows.length > 1 && (
|
||||||
|
<div className='absolute z-30 transform -translate-x-1 translate-y-1 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left rotate-1'>
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows.map(
|
||||||
|
(row) =>
|
||||||
|
row.id === activeId && (
|
||||||
|
<TableRow key={row.id} id={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
className='p-1 align-top'
|
||||||
|
key={cell.id}
|
||||||
|
style={{ width: cell.column.getSize() }}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{table.getSelectedRowModel().rows.length > 2 && (
|
||||||
|
<div className='absolute z-20 transform translate-x-1 -translate-y-1 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left -rotate-1'>
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows.map(
|
||||||
|
(row) =>
|
||||||
|
row.id === activeId && (
|
||||||
|
<TableRow key={row.id} id={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
className='p-1 align-top'
|
||||||
|
key={cell.id}
|
||||||
|
style={{ width: cell.column.getSize() }}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{table.getSelectedRowModel().rows.length > 3 && (
|
||||||
|
<div className='absolute z-10 transform translate-x-2 -translate-y-2 bg-white border rounded shadow opacity-100 hover:bg-white border-muted-foreground/50 top left rotate-2'>
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows.map(
|
||||||
|
(row) =>
|
||||||
|
row.id === activeId && (
|
||||||
|
<TableRow key={row.id} id={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
className='p-1 align-top'
|
||||||
|
key={cell.id}
|
||||||
|
style={{ width: cell.column.getSize() }}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DragOverlay>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</DndContext>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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<unknown>;
|
||||||
|
}) => {
|
||||||
|
const selectedRowsCount = table.getSelectedRowModel().rows.length;
|
||||||
|
|
||||||
|
if (selectedRowsCount) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center h-12 p-1 text-white rounded-md bg-primary ">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button>{`${selectedRowsCount} filas seleccionadas`}</Button>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
disabled={!table.getSelectedRowModel().rows.length}
|
||||||
|
onClick={() => table.options.meta?.duplicateItems()}
|
||||||
|
>
|
||||||
|
<CopyPlusIcon className="w-4 h-4" />
|
||||||
|
<span className="sm:sr-only">Duplicar</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Duplica las fila(s) seleccionadas(s)
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
disabled={!table.getSelectedRowModel().rows.length}
|
||||||
|
onClick={() => table.options.meta?.deleteItems()}
|
||||||
|
>
|
||||||
|
<Trash2Icon className="w-4 h-4" />
|
||||||
|
<span className="sm:sr-only">Eliminar</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Elimina las fila(s) seleccionada(s)</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Separator orientation="vertical" className="h-6 mx-1 bg-muted/50" />
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled={!table.getSelectedRowModel().rows.length}
|
||||||
|
onClick={() => table.resetRowSelection()}
|
||||||
|
>
|
||||||
|
<ScanIcon className="w-4 h-4 md:mr-2" />
|
||||||
|
<span className="sm:sr-only">Quitar selección</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Quita la selección</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center h-12 p-1 rounded-md bg-muted/50 text-muted-foreground">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => table.options.meta?.appendItem()}
|
||||||
|
>
|
||||||
|
<CirclePlusIcon className="w-4 h-4" />
|
||||||
|
<span className="sr-only">Añadir</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Añadir fila</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
className="h-6 mx-1 bg-muted-foreground/30"
|
||||||
|
/>
|
||||||
|
<Tooltip>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<ClockIcon className="w-4 h-4" />
|
||||||
|
<span className="sr-only">Snooze</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="flex w-[535px] p-0">
|
||||||
|
<div className="flex flex-col gap-2 px-2 py-4 border-r">
|
||||||
|
<div className="px-4 text-sm font-medium">Snooze until</div>
|
||||||
|
<div className="grid min-w-[250px] gap-1">
|
||||||
|
<Button variant="ghost" className="justify-start font-normal">
|
||||||
|
Later today{" "}
|
||||||
|
<span className="ml-auto text-muted-foreground"></span>
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" className="justify-start font-normal">
|
||||||
|
Tomorrow
|
||||||
|
<span className="ml-auto text-muted-foreground"></span>
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" className="justify-start font-normal">
|
||||||
|
This weekend
|
||||||
|
<span className="ml-auto text-muted-foreground"></span>
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" className="justify-start font-normal">
|
||||||
|
Next week
|
||||||
|
<span className="ml-auto text-muted-foreground"></span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-2">
|
||||||
|
<CalendarIcon />
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<TooltipContent>Snooze</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 ml-auto">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<ReplyIcon className="w-4 h-4" />
|
||||||
|
<span className="sr-only">Reply</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Reply</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<ReplyAllIcon className="w-4 h-4" />
|
||||||
|
<span className="sr-only">Reply all</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Reply all</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<ForwardIcon className="w-4 h-4" />
|
||||||
|
<span className="sr-only">Forward</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Forward</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<Separator
|
||||||
|
orientation="vertical"
|
||||||
|
className="h-6 mx-1 bg-muted-foreground/30"
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<MoreVerticalIcon className="w-4 h-4" />
|
||||||
|
<span className="sr-only">Columnas</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{table.getAllColumns().map((column) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={column.id}
|
||||||
|
disabled={!column.getCanHide()}
|
||||||
|
className="capitalize"
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||||
|
>
|
||||||
|
{column.id}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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<string, any>;
|
||||||
|
listeners: DraggableSyntheticListeners;
|
||||||
|
ref(node: HTMLElement | null): void;
|
||||||
|
}
|
||||||
|
export const SortableTableRowContext = createContext<Context>({
|
||||||
|
attributes: {},
|
||||||
|
listeners: undefined,
|
||||||
|
ref() {},
|
||||||
|
});
|
||||||
|
|
||||||
|
function animateLayoutChanges(args) {
|
||||||
|
if (args.isSorting || args.wasDragging) {
|
||||||
|
return defaultAnimateLayoutChanges(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortableTableRow({
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<SortableProps>) {
|
||||||
|
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 (
|
||||||
|
<SortableTableRowContext.Provider value={context}>
|
||||||
|
<TableRow
|
||||||
|
key={id}
|
||||||
|
id={String(id)}
|
||||||
|
className={cn(
|
||||||
|
isDragging ? "opacity-40" : "opacity-100",
|
||||||
|
"hover:bg-muted/30 m-0",
|
||||||
|
)}
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</TableRow>
|
||||||
|
</SortableTableRowContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
client/src/components/SorteableDataTable/index.tsx
Normal file
1
client/src/components/SorteableDataTable/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./SortableDataTable";
|
||||||
@ -1,8 +1,14 @@
|
|||||||
|
export * from "./ButtonGroup";
|
||||||
export * from "./Container";
|
export * from "./Container";
|
||||||
|
export * from "./CustomButtons";
|
||||||
export * from "./CustomDialog";
|
export * from "./CustomDialog";
|
||||||
|
export * from "./DataTable";
|
||||||
|
export * from "./EmptyState";
|
||||||
|
export * from "./ErrorOverlay";
|
||||||
export * from "./Forms";
|
export * from "./Forms";
|
||||||
export * from "./Layout";
|
export * from "./Layout";
|
||||||
export * from "./LoadingIndicator";
|
export * from "./LoadingIndicator";
|
||||||
export * from "./LoadingOverlay";
|
export * from "./LoadingOverlay";
|
||||||
export * from "./ProtectedRoute";
|
export * from "./ProtectedRoute";
|
||||||
|
export * from "./SorteableDataTable";
|
||||||
export * from "./TailwindIndicator";
|
export * from "./TailwindIndicator";
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
/*export * from "./useBreadcrumbs";
|
/*export * from "./useBreadcrumbs";
|
||||||
export * from "./useDataSource";
|
export * from "./useDataSource";
|
||||||
export * from "./useDataTable";
|
|
||||||
export * from "./useForm";
|
export * from "./useForm";
|
||||||
export * from "./useLoadingOvertime";
|
export * from "./useLoadingOvertime";
|
||||||
export * from "./useMounted";
|
export * from "./useMounted";
|
||||||
@ -13,4 +13,6 @@ export * from "./useUrlId";
|
|||||||
|
|
||||||
export * from "./useAuth";
|
export * from "./useAuth";
|
||||||
export * from "./useCustomDialog";
|
export * from "./useCustomDialog";
|
||||||
|
export * from "./useDataTable";
|
||||||
|
export * from "./usePagination";
|
||||||
export * from "./useTheme";
|
export * from "./useTheme";
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { PropsWithChildren, createContext } from "react";
|
import { PropsWithChildren, createContext } from "react";
|
||||||
import { IDataSource } from "./DataSource";
|
import { IDataSource } from "./DataSource";
|
||||||
|
|
||||||
export const DataSourceContext = createContext<IDataSource | null>(null);
|
export const DataSourceContext = createContext<IDataSource | undefined>(undefined);
|
||||||
|
|
||||||
export const DataSourceProvider = ({
|
export const DataSourceProvider = ({
|
||||||
dataSource,
|
dataSource,
|
||||||
|
|||||||
21
client/src/lib/hooks/useDataTable/DataTableContext.tsx
Normal file
21
client/src/lib/hooks/useDataTable/DataTableContext.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { usePaginationParams } from "@/lib/hooks";
|
||||||
|
import { PropsWithChildren, createContext } from "react";
|
||||||
|
|
||||||
|
export interface IDataTableContextState {}
|
||||||
|
|
||||||
|
export const DataTableContext = createContext<IDataTableContextState | null>(null);
|
||||||
|
|
||||||
|
export const DataTableProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const [pagination, setPagination] = usePaginationParams();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTableContext.Provider
|
||||||
|
value={{
|
||||||
|
pagination,
|
||||||
|
setPagination,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DataTableContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
export * from "./DataTableContext";
|
||||||
export * from "./useDataTable";
|
export * from "./useDataTable";
|
||||||
export * from "./useDataTableColumns";
|
export * from "./useDataTableColumns";
|
||||||
|
export * from "./useDataTableContext";
|
||||||
export * from "./useQueryDataTable";
|
export * from "./useQueryDataTable";
|
||||||
|
|||||||
12
client/src/lib/hooks/useDataTable/types.ts
Normal file
12
client/src/lib/hooks/useDataTable/types.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface Option {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
icon?: React.ComponentType<{ className?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataTableFilterField<TData> {
|
||||||
|
label: string;
|
||||||
|
value: keyof TData;
|
||||||
|
placeholder?: string;
|
||||||
|
options?: Option[];
|
||||||
|
}
|
||||||
@ -16,8 +16,8 @@ import {
|
|||||||
import { getDataTableSelectionColumn } from "@/components";
|
import { getDataTableSelectionColumn } from "@/components";
|
||||||
import React, { useCallback, useMemo, useState } from "react";
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { DataTableFilterField } from "../../types";
|
import { DataTableFilterField } from "./types";
|
||||||
import { usePaginationParams } from "../usePagination";
|
import { useDataTableContext } from "./useDataTableContext";
|
||||||
|
|
||||||
//import { useDebounce } from "@/hooks/use-debounce";
|
//import { useDebounce } from "@/hooks/use-debounce";
|
||||||
|
|
||||||
@ -110,13 +110,17 @@ export function useDataTable<TData, TValue>({
|
|||||||
enableSorting = false,
|
enableSorting = false,
|
||||||
enableHiding = false,
|
enableHiding = false,
|
||||||
enableRowSelection = false,
|
enableRowSelection = false,
|
||||||
|
onPaginationChange,
|
||||||
filterFields = [],
|
filterFields = [],
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
enableAdvancedFilter = false,
|
enableAdvancedFilter = false,
|
||||||
}: UseDataTableProps<TData, TValue>) {
|
}: UseDataTableProps<TData, TValue>) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const [pagination, setPagination] = usePaginationParams();
|
const { pagination, setPagination } = useDataTableContext();
|
||||||
|
console.log("pagination TABLA =>", pagination);
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
// Memoize computation of searchableColumns and filterableColumns
|
// Memoize computation of searchableColumns and filterableColumns
|
||||||
@ -149,12 +153,8 @@ export function useDataTable<TData, TValue>({
|
|||||||
const initialColumnFilters: ColumnFiltersState = useMemo(() => {
|
const initialColumnFilters: ColumnFiltersState = useMemo(() => {
|
||||||
return Array.from(searchParams.entries()).reduce<ColumnFiltersState>(
|
return Array.from(searchParams.entries()).reduce<ColumnFiltersState>(
|
||||||
(filters, [key, value]) => {
|
(filters, [key, value]) => {
|
||||||
const filterableColumn = filterableColumns.find(
|
const filterableColumn = filterableColumns.find((column) => column.value === key);
|
||||||
(column) => column.value === key
|
const searchableColumn = searchableColumns.find((column) => column.value === key);
|
||||||
);
|
|
||||||
const searchableColumn = searchableColumns.find(
|
|
||||||
(column) => column.value === key
|
|
||||||
);
|
|
||||||
|
|
||||||
if (filterableColumn) {
|
if (filterableColumn) {
|
||||||
filters.push({
|
filters.push({
|
||||||
@ -177,8 +177,7 @@ export function useDataTable<TData, TValue>({
|
|||||||
// Table states
|
// Table states
|
||||||
const [rowSelection, setRowSelection] = React.useState({});
|
const [rowSelection, setRowSelection] = React.useState({});
|
||||||
|
|
||||||
const [columnVisibility, setColumnVisibility] =
|
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
||||||
React.useState<VisibilityState>({});
|
|
||||||
|
|
||||||
const [columnFilters, setColumnFilters] =
|
const [columnFilters, setColumnFilters] =
|
||||||
React.useState<ColumnFiltersState>(initialColumnFilters);
|
React.useState<ColumnFiltersState>(initialColumnFilters);
|
||||||
@ -186,7 +185,6 @@ export function useDataTable<TData, TValue>({
|
|||||||
const paginationUpdater: OnChangeFn<PaginationState> = (updater) => {
|
const paginationUpdater: OnChangeFn<PaginationState> = (updater) => {
|
||||||
if (typeof updater === "function") {
|
if (typeof updater === "function") {
|
||||||
const newPagination = updater(pagination);
|
const newPagination = updater(pagination);
|
||||||
console.log(newPagination);
|
|
||||||
setPagination(newPagination);
|
setPagination(newPagination);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
75
client/src/lib/hooks/usePagination/usePagination.test.ts
Normal file
75
client/src/lib/hooks/usePagination/usePagination.test.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -6,7 +6,7 @@ export const INITIAL_PAGE_SIZE = 10;
|
|||||||
export const MIN_PAGE_INDEX = 0;
|
export const MIN_PAGE_INDEX = 0;
|
||||||
export const MIN_PAGE_SIZE = 1;
|
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];
|
export const DEFAULT_PAGE_SIZES = [10, 25, 50, 100];
|
||||||
|
|
||||||
@ -31,19 +31,23 @@ export const usePagination = (
|
|||||||
|
|
||||||
const updatePagination = (newPagination: PaginationState) => {
|
const updatePagination = (newPagination: PaginationState) => {
|
||||||
// Realiza comprobaciones antes de actualizar el estado
|
// Realiza comprobaciones antes de actualizar el estado
|
||||||
|
const validatedPagination = newPagination;
|
||||||
|
|
||||||
if (newPagination.pageIndex < INITIAL_PAGE_INDEX) {
|
if (validatedPagination.pageIndex < INITIAL_PAGE_INDEX) {
|
||||||
newPagination.pageIndex = INITIAL_PAGE_INDEX;
|
validatedPagination.pageIndex = INITIAL_PAGE_INDEX;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (newPagination.pageSize < MIN_PAGE_SIZE || newPagination.pageSize > MAX_PAGE_SIZE) {
|
||||||
newPagination.pageSize < MIN_PAGE_SIZE ||
|
validatedPagination.pageSize = MIN_PAGE_SIZE;
|
||||||
newPagination.pageSize > MAX_PAGE_SIZE
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setPagination(newPagination);
|
setPagination((oldPagination) => ({
|
||||||
|
...oldPagination,
|
||||||
|
pageIndex: newPagination.pageIndex,
|
||||||
|
pageSize: newPagination.pageSize,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return validatedPagination;
|
||||||
};
|
};
|
||||||
|
|
||||||
return [pagination, updatePagination] as const;
|
return [pagination, updatePagination] as const;
|
||||||
|
|||||||
@ -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 { useSearchParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
INITIAL_PAGE_INDEX,
|
INITIAL_PAGE_INDEX,
|
||||||
INITIAL_PAGE_SIZE,
|
INITIAL_PAGE_SIZE,
|
||||||
|
MAX_PAGE_SIZE,
|
||||||
|
MIN_PAGE_SIZE,
|
||||||
usePagination,
|
usePagination,
|
||||||
} from "./usePagination";
|
} from "./usePagination";
|
||||||
|
|
||||||
@ -17,24 +20,29 @@ export const usePaginationParams = (
|
|||||||
|
|
||||||
const calculatedPageIndex = useMemo(() => {
|
const calculatedPageIndex = useMemo(() => {
|
||||||
const parsedPageIndex = parseInt(urlParamPageIndex ?? "", 10);
|
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]);
|
}, [urlParamPageIndex, initialPageIndex]);
|
||||||
|
|
||||||
const calculatedPageSize = useMemo(() => {
|
const calculatedPageSize = useMemo(() => {
|
||||||
const parsedPageSize = parseInt(urlParamPageSize ?? "", 10);
|
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]);
|
}, [urlParamPageSize, initialPageSize]);
|
||||||
|
|
||||||
const [pagination, setPagination] = usePagination(
|
const [pagination, setPagination] = usePagination(calculatedPageIndex, calculatedPageSize);
|
||||||
calculatedPageIndex,
|
|
||||||
calculatedPageSize
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
/*useEffect(() => {
|
||||||
// Actualizar la URL cuando cambia la paginación
|
// Actualizar la URL cuando cambia la paginación
|
||||||
const actualSearchParam = Object.fromEntries(
|
const actualSearchParam = Object.fromEntries(new URLSearchParams(urlSearchParams));
|
||||||
new URLSearchParams(urlSearchParams)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
String(pagination.pageIndex) !== actualSearchParam.page_index ||
|
String(pagination.pageIndex) !== actualSearchParam.page_index ||
|
||||||
@ -46,7 +54,17 @@ export const usePaginationParams = (
|
|||||||
page_size: String(pagination.pageSize),
|
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];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,14 +1,36 @@
|
|||||||
{
|
{
|
||||||
"translation": {
|
"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": {
|
"login_page": {
|
||||||
"title": "Presupuestador para distribuidores",
|
"title": "Uecko Quotes",
|
||||||
"description": "Introduzca su dirección de correo electrónico y contraseña para acceder",
|
"description": "Enter your email address and password to login",
|
||||||
"email_label": "Email",
|
"email_label": "Email",
|
||||||
"email_placeholder": "micorreo@ejemplo.com",
|
"email_placeholder": "user@sample.com",
|
||||||
"password_label": "Password",
|
"password_label": "Password",
|
||||||
"forgotten_password": "¿Has olvidado tu contraseña?",
|
"forgotten_password": "Forgot your password?",
|
||||||
"become_dealer": "¿Quieres ser distribuidor de Uecko?",
|
"become_dealer": "Do you want to become a Uecko dealer?",
|
||||||
"contact_us": "Contact us",
|
"contact_us": "Contact us",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"error": "Error"
|
"error": "Error"
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
"dealers": "Distribuidores",
|
"dealers": "Distribuidores",
|
||||||
"catalog": "Catálogo",
|
"catalog": "Catálogo",
|
||||||
"quotes": "Cotizaciones",
|
"quotes": "Cotizaciones",
|
||||||
"search_placeholder": "Buscar productos, distribuidores, etc...",
|
"search_placeholder": "Buscar productos, cotizaciones, etc...",
|
||||||
"user": {
|
"user": {
|
||||||
"user_menu": "Menú del usuario",
|
"user_menu": "Menú del usuario",
|
||||||
"my_account": "Mi cuenta",
|
"my_account": "Mi cuenta",
|
||||||
@ -34,6 +34,12 @@
|
|||||||
"contact_us": "Contacta con nosotros",
|
"contact_us": "Contacta con nosotros",
|
||||||
"login": "Entrar",
|
"login": "Entrar",
|
||||||
"error": "Error"
|
"error": "Error"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"welcome": "Bienvenido"
|
||||||
|
},
|
||||||
|
"catalog": {
|
||||||
|
"title": "Catálogo de artículos"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user