This commit is contained in:
David Arranz 2024-08-12 20:22:34 +02:00
parent bb3ab3dbb4
commit 09cbfc12c5
20 changed files with 429 additions and 311 deletions

View File

@ -2,6 +2,7 @@ import { Outlet, RouterProvider, createBrowserRouter } from "react-router-dom";
import {
DealerLayout,
DealersList,
ErrorPage,
LoginPage,
LogoutPage,
QuoteCreate,
@ -25,6 +26,13 @@ export const Routes = () => {
},
];
const routesForErrors = [
{
path: "*",
Component: ErrorPage,
},
];
// Define routes accessible only to authenticated users
const routesForAuthenticatedOnly = [
{
@ -123,7 +131,12 @@ export const Routes = () => {
// Combine and conditionally include routes based on authentication status
const router = createBrowserRouter(
[...routesForPublic, ...routesForAuthenticatedOnly, ...routesForNotAuthenticatedOnly],
[
...routesForPublic,
...routesForAuthenticatedOnly,
...routesForNotAuthenticatedOnly,
...routesForErrors,
],
{
//basename: "/app",
}

View File

@ -25,6 +25,29 @@ export const ErrorPage = (props: ErrorPageProps) => {
try again.
</p>
);
return (
<div className='flex min-h-[100dvh] flex-col items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8'>
<div className='max-w-md mx-auto text-center'>
<div className='w-12 h-12 mx-auto text-primary' />
<h1 className='mt-4 text-3xl font-bold tracking-tight text-foreground sm:text-4xl'>
Oops, page not found!
</h1>
<p className='mt-4 text-muted-foreground'>
The page you're looking for doesn't exist or has been moved.
</p>
<div className='mt-6'>
<Button
className='inline-flex items-center px-4 py-2 text-sm font-medium transition-colors rounded-md shadow-sm bg-primary text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2'
prefetch={false}
>
Go to Homepage
</Button>
</div>
</div>
</div>
);
return (
<section id='Error' className='flex flex-col items-center w-full h-full'>
{msg}

View File

@ -0,0 +1,31 @@
import { cn } from "@/lib/utils";
import { Button, ButtonProps } from "@/ui";
import { t } from "i18next";
import { PackagePlusIcon } from "lucide-react";
export interface AppendCatalogArticleRowButtonProps extends ButtonProps {
label?: string;
className?: string;
}
export const AppendCatalogArticleRowButton = ({
label = t("common.append_article"),
className,
...props
}: AppendCatalogArticleRowButtonProps): JSX.Element => (
<Button
type='button'
variant='outline'
size='icon'
className={cn(
"w-full gap-1 border-dashed text-muted-foreground border-muted-foreground/50",
className
)}
{...props}
>
<PackagePlusIcon className={label ? "w-4 h-4 mr-2" : "w-4 h-4"} />
{label && <>{label}</>}
</Button>
);
AppendCatalogArticleRowButton.displayName = "AddNewRowButton";

View File

@ -1,17 +1,18 @@
import { cn } from "@/lib/utils";
import { Button, ButtonProps } from "@/ui";
import { t } from "i18next";
import { PlusCircleIcon } from "lucide-react";
export interface AddNewRowButtonProps extends ButtonProps {
export interface AppendEmptyRowButtonProps extends ButtonProps {
label?: string;
className?: string;
}
export const AddNewRowButton = ({
label = "Añade nueva fila",
export const AppendEmptyRowButton = ({
label = t("common.append_empty_row"),
className,
...props
}: AddNewRowButtonProps): JSX.Element => (
}: AppendEmptyRowButtonProps): JSX.Element => (
<Button
type='button'
variant='outline'
@ -27,4 +28,4 @@ export const AddNewRowButton = ({
</Button>
);
AddNewRowButton.displayName = "AddNewRowButton";
AppendEmptyRowButton.displayName = "AddNewRowButton";

View File

@ -1,5 +1,3 @@
import { Card, CardContent } from "@/ui";
import { DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
import { useCatalogList } from "@/app/catalog/hooks";
@ -13,7 +11,7 @@ import { t } from "i18next";
import { useMemo } from "react";
import { useNavigate } from "react-router-dom";
export const CatalogPickerDataTable = ({ onClick }: { onClick: (data: unknown) => void }) => {
export const CatalogPickerDataTable = ({ onSelect }: { onSelect: (data: unknown) => void }) => {
const navigate = useNavigate();
const { pagination, globalFilter, isFiltered } = useDataTableContext();
@ -48,7 +46,7 @@ export const CatalogPickerDataTable = ({ onClick }: { onClick: (data: unknown) =
onClick={
(event) => {
event.preventDefault();
onClick && onClick(row.original);
onSelect && onSelect(row.original);
}
/*setMail({
...mail,
@ -57,10 +55,10 @@ export const CatalogPickerDataTable = ({ onClick }: { onClick: (data: unknown) =
}
>
<div className='flex flex-row justify-between w-full space-x-6'>
<div className='text-left grow line-clamp-2 text-muted-foreground hover:text-foreground'>
<div className='w-3/4 text-left grow line-clamp-2 text-muted-foreground hover:text-foreground'>
{renderValue()}
</div>
<div className='text-right'>
<div className='w-1/4 text-right'>
<dl className='flex flex-row justify-end space-x-1'>
<dt className='text-xs font-medium text-accent-foreground/75'>
{t("catalog.list.columns.points")}:
@ -94,17 +92,13 @@ export const CatalogPickerDataTable = ({ onClick }: { onClick: (data: unknown) =
if (isPending) {
return (
<Card>
<CardContent>
<DataTableSkeleton
columnCount={6}
searchableColumnCount={1}
filterableColumnCount={2}
//cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem"]}
shrinkZero
/>
</CardContent>
</Card>
<DataTableSkeleton
columnCount={6}
searchableColumnCount={1}
filterableColumnCount={2}
//cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem"]}
shrinkZero
/>
);
}
@ -120,10 +114,12 @@ export const CatalogPickerDataTable = ({ onClick }: { onClick: (data: unknown) =
return (
<DataTable
className='bg-transparent border-0 shadow-none'
table={table}
headerOptions={{ visible: false }}
paginationOptions={{ visible: true }}
title='Catálogo'
paginationOptions={{ visible: true, enablePageSizeSelector: false }}
contentClassName='p-0'
footerClassName='p-0'
rowClassName='border-b-0'
cellClassName='px-0'
>

View File

@ -38,10 +38,11 @@ import {
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { useCallback, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { FieldValues, UseFieldArrayReturn } from "react-hook-form";
import { AddNewRowButton } from "./AddNewRowButton";
import { AppendCatalogArticleRowButton } from "./AppendCatalogArticleRowButton";
import { AppendEmptyRowButton } from "./AppendEmptyRowButton";
import { QuoteItemsSortableDataTableToolbar } from "./QuoteItemsSortableDataTableToolbar";
import { QuoteItemsSortableTableRow } from "./QuoteItemsSortableTableRow";
@ -49,6 +50,7 @@ declare module "@tanstack/react-table" {
interface TableMeta<TData extends RowData> {
insertItem: (rowIndex: number, data?: unknown) => void;
appendItem: (data?: unknown) => void;
pickCatalogArticle: () => void;
duplicateItems: (rowIndex?: number) => void;
deleteItems: (rowIndex?: number | number[]) => void;
updateItem: (rowIndex: number, rowData: TData, fieldName: string, value: unknown) => void;
@ -63,7 +65,7 @@ export type QuoteItemsSortableDataTableProps = {
columns: ColumnDef<unknown, unknown>[];
data: Record<"id", string>[];
defaultValues: Readonly<{ [x: string]: any }> | undefined;
actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields">;
actions: Omit<UseFieldArrayReturn<FieldValues, "items">, "fields"> & Record<string, unknown>;
};
const measuringConfig = {
@ -139,6 +141,9 @@ export function QuoteItemsSortableDataTable({
appendItem: (data?: unknown) => {
actions.append(data || defaultValues?.items[0], { shouldFocus: true });
},
pickCatalogArticle: () => {
actions.pickCatalogArticle();
},
duplicateItems: (rowIndex?: number) => {
if (rowIndex != undefined) {
const originalData = table.getRowModel().rows[rowIndex].original;
@ -146,6 +151,7 @@ export function QuoteItemsSortableDataTable({
} 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 }));
@ -238,70 +244,6 @@ export function QuoteItemsSortableDataTable({
setActiveId(null);
}
const hadleNewItem = useCallback(() => {
actions.append([
{
article_id: 2000004503,
description: "Lacquered Norma with 1 spline for lacquered panel up to 700 mm (27.56 in)",
quantity: { amount: "15", scale: 0 },
unit_price: {
amount: "150000",
scale: 4,
currency_code: "EUR",
},
discount: {
amount: 3500,
scale: 2,
},
},
{
article_id: 2000005891,
description:
"Split walnut HPL 3 elephant gray faux-leather central earring tray compartment 150x410x50 mm (5.91 in x 16.14 in x 1.97 in)",
quantity: { amount: "8", scale: 0 },
unit_price: {
amount: "384560",
scale: 4,
currency_code: "EUR",
},
discount: {
amount: null,
scale: 2,
},
},
{
article_id: 2000007412,
description:
"Nara H=3000 mm (118.11 in) fabric-covered glass panel up to 600 mm (23.62 in) wide",
quantity: { amount: "4", scale: 0 },
unit_price: {
amount: "8450000",
scale: 4,
currency_code: "EUR",
},
discount: {
amount: 500,
scale: 2,
},
},
{
article_id: 2000002589,
description:
"Panoramic anodized sliding H=2600 mm (102.36 in) GR3 glass panel up to 1200 mm (47.24 in) wide",
quantity: { amount: "25", scale: 0 },
unit_price: {
amount: "67481",
scale: 4,
currency_code: "EUR",
},
discount: {
amount: 100,
scale: 2,
},
},
]);
}, [actions]);
function filterItems(items: string[] | Row<unknown>[]) {
if (!activeId) {
return items;
@ -357,9 +299,16 @@ export function QuoteItemsSortableDataTable({
</TableBody>
<TableFooter className='bg-default'>
<TableRow className='hover:bg-default'>
<TableCell colSpan={6} className='py-6'>
<AddNewRowButton onClick={hadleNewItem} />
<TableCell colSpan={3} className='py-6'></TableCell>
<TableCell colSpan={5} className='py-6'>
<div className='grid grid-cols-2 gap-6'>
<AppendEmptyRowButton onClick={() => table.options.meta?.appendItem()} />
<AppendCatalogArticleRowButton
onClick={() => table.options.meta?.pickCatalogArticle()}
/>
</div>
</TableCell>
<TableCell className='py-6'></TableCell>
</TableRow>
</TableFooter>
</Table>

View File

@ -8,81 +8,95 @@ export const QuoteItemsSortableDataTableToolbar = ({ table }: { table: Table<unk
if (selectedRowsCount) {
return (
<div className='flex items-center h-12 p-1 text-white rounded-md bg-primary '>
<nav className='sticky z-10 pt-4 bg-background top-16'>
<div className='flex items-center h-12 p-1 rounded-md text-muted-foreground bg-primary '>
<div className='flex items-center gap-2'>
<Tooltip>
<TooltipTrigger asChild>
<Button
type='button'
variant='ghost'
disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.options.meta?.duplicateItems()}
>
<CopyPlusIcon className='w-4 h-4 sm:mr-2' />
<span className='sr-only sm:not-sr-only'>
{t("common.duplicate_selected_rows")}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.duplicate_selected_rows_tooltip")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type='button'
variant='ghost'
disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.options.meta?.deleteItems()}
>
<Trash2Icon className='w-4 h-4 sm:mr-2' />
<span className='sr-only sm:not-sr-only'>{t("common.remove_selected_rows")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.remove_selected_rows_tooltip")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type='button'
variant='ghost'
disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.resetRowSelection()}
>
<ScanIcon className='w-4 h-4 sm:mr-2' />
<span className='sr-only sm:not-sr-only'>{t("common.reset_selected_rows")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.reset_selected_rows_tooltip")}</TooltipContent>
</Tooltip>
<Separator orientation='vertical' className='h-6 mx-1 bg-muted-foreground' />
<p>{t("common.rows_selected", { count: selectedRowsCount })}</p>
</div>
</div>
</nav>
);
}
return (
<nav className='sticky z-10 pt-4 bg-background top-16'>
<div className='flex items-center h-12 p-1 rounded-md bg-accent text-muted-foreground'>
<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()}
onClick={() => table.options.meta?.appendItem()}
>
<CopyPlusIcon className='w-4 h-4' />
<span className='sm:sr-only'>{t("common.duplicate_rows")}</span>
<CirclePlusIcon className='w-4 h-4 mr-2' />
<span>{t("common.append_empty_row")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.duplicate_rows_tooltip")}</TooltipContent>
<TooltipContent>{t("common.append_empty_row_tooltip")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
type='button'
variant='ghost'
size='icon'
disabled={!table.getSelectedRowModel().rows.length}
onClick={() => table.options.meta?.deleteItems()}
onClick={() => table.options.meta?.pickCatalogArticle()}
>
<Trash2Icon className='w-4 h-4' />
<span className='sm:sr-only'>Eliminar</span>
<PackagePlusIcon className='w-4 h-4 mr-2' />
<span>{t("common.append_article")}</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>
<TooltipContent>{t("common.append_article_tooltip")}</TooltipContent>
</Tooltip>
</div>
<div className='flex items-center gap-2 ml-auto'></div>
</div>
);
}
return (
<div className='flex items-center h-12 p-1 rounded-md bg-accent/50 text-muted-foreground'>
<div className='flex items-center gap-2'>
<Tooltip>
<TooltipTrigger asChild>
<Button type='button' variant='ghost' onClick={() => table.options.meta?.appendItem()}>
<CirclePlusIcon className='w-4 h-4 mr-2' />
<span>{t("common.append_empty_row")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.append_empty_row_tooltip")}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button type='button' variant='ghost' onClick={() => table.options.meta?.appendItem()}>
<PackagePlusIcon className='w-4 h-4 mr-2' />
<span>{t("common.append_article")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{t("common.append_article_tooltip")}</TooltipContent>
</Tooltip>
</div>
<div className='flex items-center gap-2 ml-auto'></div>
</div>
</nav>
);
};

View File

@ -0,0 +1,28 @@
import { Button, Dialog, DialogContent, DialogFooter } from "@/ui";
import { DataTableProvider } from "@/lib/hooks";
import { CatalogPickerDataTable } from "../CatalogPickerDataTable";
export const CatalogPickerDialog = ({
isOpen,
onOpenChange,
onSelect,
}: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
onSelect: (data: unknown) => void;
}) => {
return (
<Dialog modal open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className='w-11/12 max-w-full'>
<DataTableProvider syncWithLocation={false} initialPageSize={5}>
<CatalogPickerDataTable onSelect={onSelect} />
</DataTableProvider>
<DialogFooter>
<Button type='submit'>Choose</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@ -4,6 +4,7 @@ import {
FormQuantityField,
FormTextAreaField,
} from "@/components";
import { DataTableProvider } from "@/lib/hooks";
import { cn } from "@/lib/utils";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/ui";
@ -13,7 +14,9 @@ import { t } from "i18next";
import { useCallback, useState } from "react";
import { useFieldArray, useFormContext } from "react-hook-form";
import { useDetailColumns } from "../../hooks";
import { CatalogPickerDataTable } from "../CatalogPickerDataTable";
import { QuoteItemsSortableDataTable } from "../QuoteItemsSortableDataTable";
import { CatalogPickerDialog } from "./CatalogPickerDialog";
export const QuoteDetailsCardEditor = ({
currency,
@ -26,6 +29,10 @@ export const QuoteDetailsCardEditor = ({
}) => {
const { control, register } = useFormContext();
const [pickerMode] = useState<"dialog" | "panel">("dialog");
const [pickerDialogOpen, setPickerDialogOpen] = useState<boolean>(false);
const { fields, ...fieldActions } = useFieldArray({
control,
name: "items",
@ -192,15 +199,15 @@ export const QuoteDetailsCardEditor = ({
}
);
const handleInsertArticle = useCallback(
(newArticle: any) => {
const handleAppendCatalogArticle = useCallback(
(article: any) => {
fieldActions.append({
...newArticle,
...article,
quantity: {
amount: 100,
scale: Quantity.DEFAULT_SCALE,
},
unit_price: newArticle.retail_price,
unit_price: article.retail_price,
});
},
[fieldActions]
@ -211,24 +218,26 @@ export const QuoteDetailsCardEditor = ({
const defaultLayout = [265, 440, 655];
const navCollapsedSize = 4;
return (
<>
<QuoteItemsSortableDataTable
actions={fieldActions}
columns={columns}
data={fields}
defaultValues={defaultValues}
/>
<FormCurrencyField
variant='outline'
currency={currency}
language={language}
scale={4}
className='text-right'
{...register("subtotal_price")}
/>
</>
);
if (pickerMode === "dialog") {
return (
<div className='relative'>
<QuoteItemsSortableDataTable
actions={{
...fieldActions,
pickCatalogArticle: () => setPickerDialogOpen(true),
}}
columns={columns}
data={fields}
defaultValues={defaultValues}
/>
<CatalogPickerDialog
onSelect={handleAppendCatalogArticle}
isOpen={pickerDialogOpen}
onOpenChange={setPickerDialogOpen}
/>
</div>
);
}
return (
<ResizablePanelGroup
@ -250,12 +259,17 @@ export const QuoteDetailsCardEditor = ({
}}
className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")}
>
<QuoteItemsSortableDataTable actions={fieldActions} columns={columns} data={fields} />
<QuoteItemsSortableDataTable
actions={fieldActions}
columns={columns}
data={fields}
defaultValues={defaultValues}
/>
</ResizablePanel>
<ResizableHandle withHandle className='mx-3' />
<ResizablePanel defaultSize={defaultLayout[1]} minSize={10}>
<DataTableProvider syncWithLocation={false}>
<CatalogPickerDataTable onClick={handleInsertArticle} />
<CatalogPickerDataTable onSelect={handleAppendCatalogArticle} />
</DataTableProvider>
</ResizablePanel>
</ResizablePanelGroup>

View File

@ -26,7 +26,7 @@ export type DataTableColumnProps<TData, TValue> = ColumnDef<TData, TValue>;
export type DataTablePaginationOptionsProps<TData> = Pick<
DataTablePaginationProps<TData>,
"visible"
"visible" | "enablePageSizeSelector"
>;
export type DataTableHeaderOptionsProps = {
@ -41,6 +41,8 @@ export type DataTableProps<TData> = PropsWithChildren<{
paginationOptions?: DataTablePaginationOptionsProps<TData>;
headerOptions?: DataTableHeaderOptionsProps;
className?: string;
contentClassName?: string;
footerClassName?: string;
rowClassName?: string;
cellClassName?: string;
}>;
@ -54,6 +56,8 @@ export function DataTable<TData>({
headerOptions = { visible: true },
children,
className,
contentClassName,
footerClassName,
rowClassName,
cellClassName,
}: DataTableProps<TData>) {
@ -67,7 +71,7 @@ export function DataTable<TData>({
<CardDescription>{description}</CardDescription>
</CardHeader>
)}
<CardContent className='pt-6'>
<CardContent className={cn("pt-6", contentClassName)}>
{children && (
<>
<div className='flex space-x-2'>{children}</div>
@ -124,12 +128,8 @@ export function DataTable<TData>({
</TableBody>
</Table>
</CardContent>
<CardFooter>
<DataTablePagination
className='flex-1'
visible={paginationOptions?.visible}
table={table}
/>
<CardFooter className={footerClassName}>
<DataTablePagination className='flex-1' table={table} {...paginationOptions} />
</CardFooter>
</Card>
);

View File

@ -15,12 +15,14 @@ import { useMemo } from "react";
export type DataTablePaginationProps<TData> = {
table: Table<TData>;
className?: string;
enablePageSizeSelector?: boolean;
visible?: boolean | "auto";
};
export function DataTablePagination<TData>({
table,
className,
enablePageSizeSelector = true,
visible = "auto",
}: DataTablePaginationProps<TData>) {
const isVisible = useMemo(() => visible === true, [visible]);
@ -31,11 +33,11 @@ export function DataTablePagination<TData>({
}
return (
<div className={cn("flex items-center justify-between px-2", className)}>
<div className={className}>
<div className='flex-1 text-base text-muted-foreground'>
{table.getSelectedRowModel().rows.length > 0 && (
<>
{t("common.rows_selected", {
{t("common.rows_selected_of_total", {
count: table.getFilteredSelectedRowModel().rows.length,
total: table.getFilteredRowModel().rows.length,
})}
@ -43,75 +45,86 @@ export function DataTablePagination<TData>({
)}
</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'>{t("common.rows_per_page")}</p>
<div className='flex justify-between space-x-6 lg:space-x-8'>
{enablePageSizeSelector && (
<div className='flex items-center space-x-2 grow'>
<p className='text-sm font-medium'>{t("common.rows_per_page")}</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'>
{t("common.num_page_of_total", {
count: table.getState().pagination.pageIndex + 1,
total: table.getPageCount(),
})}
</div>
<div className='flex items-center space-x-2'>
<Button
type='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'>{t("common.go_to_first_page")}</span>
<ChevronsLeftIcon className='w-4 h-4' />
</Button>
<Button
type='button'
variant='outline'
className='w-8 h-8 p-0'
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className='sr-only'>{t("common.go_to_prev_page")}</span>
<ChevronLeftIcon className='w-4 h-4' />
</Button>
<Button
type='button'
variant='outline'
className='w-8 h-8 p-0'
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className='sr-only'>{t("common.go_to_next_page")}</span>
<ChevronRightIcon className='w-4 h-4' />
</Button>
<Button
type='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'>{t("common.go_to_last_page")}</span>
<ChevronsRightIcon className='w-4 h-4' />
</Button>
<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={cn(
"flex space-x-2 flex-1",
enablePageSizeSelector ? "justify-end" : "justify-between"
)}
>
<div className='flex w-[100px] items-center justify-center'>
<p className='text-sm font-medium '>
{t("common.num_page_of_total", {
count: table.getState().pagination.pageIndex + 1,
total: table.getPageCount(),
})}
</p>
</div>
<div className='flex items-center space-x-2'>
<Button
type='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'>{t("common.go_to_first_page")}</span>
<ChevronsLeftIcon className='w-4 h-4' />
</Button>
<Button
type='button'
variant='outline'
className='w-8 h-8 p-0'
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className='sr-only'>{t("common.go_to_prev_page")}</span>
<ChevronLeftIcon className='w-4 h-4' />
</Button>
<Button
type='button'
variant='outline'
className='w-8 h-8 p-0'
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className='sr-only'>{t("common.go_to_next_page")}</span>
<ChevronRightIcon className='w-4 h-4' />
</Button>
<Button
type='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'>{t("common.go_to_last_page")}</span>
<ChevronsRightIcon className='w-4 h-4' />
</Button>
</div>
</div>
</div>
</div>

View File

@ -27,12 +27,20 @@ export const DataTableContext = createContext<IDataTableContextState | null>(nul
export const DataTableProvider = ({
syncWithLocation = true,
initialGlobalFilter = "",
initialPageIndex,
initialPageSize,
children,
}: PropsWithChildren<{
syncWithLocation?: boolean;
initialGlobalFilter?: string;
initialPageIndex?: number;
initialPageSize?: number;
}>) => {
const [pagination, setPagination] = useSyncedPagination(syncWithLocation);
const [pagination, setPagination] = useSyncedPagination({
syncWithLocation,
initialPageIndex,
initialPageSize,
});
const [globalFilter, setGlobalFilter] = useState<string>(initialGlobalFilter);
const [sorting, setSorting] = useState<SortingState>([]);

View File

@ -1,8 +1,21 @@
import { usePagination, usePaginationSyncWithLocation } from "../usePagination";
export const useSyncedPagination = (syncWithLocation: boolean) => {
type UseSyncedPaginationProps = {
syncWithLocation?: boolean;
initialPageIndex?: number;
initialPageSize?: number;
};
export const useSyncedPagination = ({
syncWithLocation = true,
initialPageIndex,
initialPageSize,
}: UseSyncedPaginationProps) => {
const [paginationWithLocation, setPaginationWithLocation] = usePaginationSyncWithLocation();
const [paginationWithoutLocation, setPaginationWithoutLocation] = usePagination();
const [paginationWithoutLocation, setPaginationWithoutLocation] = usePagination(
initialPageIndex,
initialPageSize
);
if (syncWithLocation) {
return [paginationWithLocation, setPaginationWithLocation] as const;

View File

@ -7,7 +7,7 @@ import {
import { useState } from "react";
export const DEFAULT_PAGE_SIZES = [15, 30, 50, 75, 100];
export const DEFAULT_PAGE_SIZES = [5, 10, 15, 30, 50, 75, 100];
export interface PaginationState {
pageIndex: number;

View File

@ -1,6 +1,5 @@
import { CustomDialog } from "@/components";
import { NullOr } from "@shared/utilities";
import { PropsWithChildren, createContext, useCallback, useMemo, useState } from "react";
import { createContext } from "react";
import { UnsavedChangesNotifierProps } from "./useUnsavedChangesNotifier";
export interface IUnsavedWarnContextState {
@ -8,48 +7,3 @@ export interface IUnsavedWarnContextState {
}
export const UnsavedWarnContext = createContext<NullOr<IUnsavedWarnContextState>>(null);
export const UnsavedWarnProvider = ({ children }: PropsWithChildren) => {
const [confirm, setConfirm] = useState<NullOr<UnsavedChangesNotifierProps>>(null);
const [open, toggle] = useState(false);
const show = useCallback(
(confirmOptions: NullOr<UnsavedChangesNotifierProps>) => {
setConfirm(confirmOptions);
toggle(true);
},
[toggle, setConfirm]
);
const onConfirm = () => {
confirm?.onConfirm?.();
toggle(false);
};
const onCancel = () => {
confirm?.onCancel?.();
toggle(false);
};
const value = useMemo(() => ({ show }), [show]);
return (
<UnsavedWarnContext.Provider value={value}>
{children}
<CustomDialog
//type='warning'
onCancel={() => {
console.log("onCancel");
onCancel();
}}
onConfirm={() => onConfirm()}
title={confirm?.title}
description={confirm?.subtitle}
confirmLabel={confirm?.confirmText}
cancelLabel={confirm?.cancelText}
isOpen={open}
/>
</UnsavedWarnContext.Provider>
);
};

View File

@ -0,0 +1,50 @@
import { CustomDialog } from "@/components";
import { NullOr } from "@shared/utilities";
import { PropsWithChildren, useCallback, useMemo, useState } from "react";
import { UnsavedChangesNotifierProps } from "./useUnsavedChangesNotifier";
import { UnsavedWarnContext } from "./WarnAboutChangeContext";
export const UnsavedWarnProvider = ({ children }: PropsWithChildren) => {
const [confirm, setConfirm] = useState<NullOr<UnsavedChangesNotifierProps>>(null);
const [open, toggle] = useState(false);
const show = useCallback(
(confirmOptions: NullOr<UnsavedChangesNotifierProps>) => {
setConfirm(confirmOptions);
toggle(true);
},
[toggle, setConfirm]
);
const onConfirm = () => {
confirm?.onConfirm?.();
toggle(false);
};
const onCancel = () => {
confirm?.onCancel?.();
toggle(false);
};
const value = useMemo(() => ({ show }), [show]);
return (
<UnsavedWarnContext.Provider value={value}>
{children}
<CustomDialog
//type='warning'
onCancel={() => {
console.log("onCancel");
onCancel();
}}
onConfirm={() => onConfirm()}
title={confirm?.title}
description={confirm?.subtitle}
confirmLabel={confirm?.confirmText}
cancelLabel={confirm?.cancelText}
isOpen={open}
/>
</UnsavedWarnContext.Provider>
);
};

View File

@ -1,3 +1,4 @@
export * from "./WarnAboutChangeContext";
export * from "./useUnsavedChangesNotifier";
export * from "./useWarnAboutChange";
export * from "./WarnAboutChangeContext";
export * from "./WarnAboutChangeProvider";

View File

@ -17,7 +17,8 @@
"sort_desc": "Desc",
"sort_desc_description": "In descending order. Click to sort in ascending order.",
"sort_none_description": "No sorting order. Click to sort in ascending order.",
"rows_selected": "{{count}} of {{total}} row(s) selected.",
"rows_selected": "{{count}} row(s) selected.",
"rows_selected_of_total": "{{count}} of {{total}} row(s) selected.",
"rows_per_page": "Rows per page",
"num_page_of_total": "Page {{count}} of {{total}}",
"go_to_first_page": "Go to first page",
@ -29,13 +30,17 @@
"error": "Error",
"actions": "Actions",
"open_menu": "Open menu",
"duplicate_rows": "Duplicate",
"duplicate_rows_tooltip": "Duplicate selected row(s)",
"duplicate_selected_rows": "Duplicate",
"duplicate_selected_rows_tooltip": "Duplicate selected row(s)",
"append_empty_row": "Append row",
"append_empty_row_tooltip": "Append a empty row",
"append_article": "Append article",
"append_article_tooltip": "Select and add an item from the catalog",
"remove_row": "Remove",
"remove_selected_rows": "Remove",
"remove_selected_rows_tooltip": "Remove selected row(s)",
"reset_selected_rows": "Reset selection",
"reset_selected_rows_tooltip": "Reset selected row(s)",
"insert_row_above": "Insert row above",
"insert_row_below": "Insert row below",
"pick_date": "Select a date",

View File

@ -17,7 +17,8 @@
"sort_desc": "Desc",
"sort_desc_description": "En orden descendente. Click para ordenar ascendentemente.",
"sort_none_description": "Sin orden. Click para ordenar ascendentemente.",
"rows_selected": "{{count}} de {{total}} fila(s) seleccionadas.",
"rows_selected": "{{count}} fila(s) seleccionadas.",
"rows_selected_of_total": "{{count}} de {{total}} fila(s) seleccionadas.",
"rows_per_page": "Filas por página",
"num_page_of_total": "Página {{count}} de {{total}}",
"go_to_first_page": "Ir a la primera página",
@ -29,13 +30,17 @@
"error": "Error",
"actions": "Acciones",
"open_menu": "Abrir el menú",
"duplicate_rows": "Duplicar",
"duplicate_rows_tooltip": "Duplica las fila(s) seleccionadas(s)",
"duplicate_selected_rows": "Duplicar",
"duplicate_selected_rows_tooltip": "Duplica las fila(s) seleccionadas(s)",
"append_empty_row": "Añadir fila",
"append_empty_row_tooltip": "Añadir una fila vacía",
"append_article": "Añadir artículo",
"append_article_tooltip": "Elegir un artículo del catálogo y añadirlo",
"remove_row": "Eliminar",
"remove_selected_rows": "Eliminar",
"remove_selected_rows_tooltip": "Elimina las fila(s) seleccionadas(s)",
"reset_selected_rows": "Quitar selection",
"reset_selected_rows_tooltip": "Dejar de seleccionar la(s) fila(s)",
"insert_row_above": "Insertar fila encima",
"insert_row_below": "Insertar fila debajo",
"pick_date": "Elige una fecha",

View File

@ -1,5 +1,5 @@
export const INITIAL_PAGE_INDEX = 0;
export const INITIAL_PAGE_SIZE = 15;
export const INITIAL_PAGE_SIZE = 5;
export const MIN_PAGE_INDEX = 0;
export const MIN_PAGE_SIZE = 1;