.
This commit is contained in:
parent
1caa778374
commit
6ead69908c
@ -27,7 +27,6 @@ export const useCatalogList = (params: UseCatalogListParams): UseCatalogListResp
|
||||
return useList({
|
||||
queryKey: keys().data().resource("catalog").action("list").params(params).get(),
|
||||
queryFn: () => {
|
||||
console.log(pagination);
|
||||
return dataSource.getList({
|
||||
resource: "catalog",
|
||||
quickSearchTerm: searchTerm,
|
||||
|
||||
87
client/src/app/dealers/components/DealerDataTable.tsx
Normal file
87
client/src/app/dealers/components/DealerDataTable.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import { Card, CardContent } from "@/ui";
|
||||
|
||||
import { DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
|
||||
|
||||
import { DataTable } from "@/components";
|
||||
import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
|
||||
import { useDataTable, useDataTableContext } from "@/lib/hooks";
|
||||
import { IListArticles_Response_DTO } from "@shared/contexts";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useDealerList } from "../hooks";
|
||||
|
||||
export const DealerDataTable = () => {
|
||||
const navigate = useNavigate();
|
||||
const { pagination, globalFilter, isFiltered } = useDataTableContext();
|
||||
|
||||
const { data, isPending, isError, error } = useDealerList({
|
||||
pagination: {
|
||||
pageIndex: pagination.pageIndex,
|
||||
pageSize: pagination.pageSize,
|
||||
},
|
||||
searchTerm: globalFilter,
|
||||
});
|
||||
|
||||
const columns = useMemo<ColumnDef<IListArticles_Response_DTO, any>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "id" as const,
|
||||
accessorKey: "id",
|
||||
enableResizing: false,
|
||||
size: 10,
|
||||
},
|
||||
{
|
||||
id: "name" as const,
|
||||
accessorKey: "name",
|
||||
enableResizing: false,
|
||||
size: 10,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: data?.items ?? [],
|
||||
columns: columns,
|
||||
pageCount: data?.total_pages ?? -1,
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
return <ErrorOverlay subtitle={(error as Error).message} />;
|
||||
}
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<DataTableSkeleton
|
||||
columnCount={6}
|
||||
searchableColumnCount={1}
|
||||
filterableColumnCount={2}
|
||||
//cellWidths={["10rem", "40rem", "12rem", "12rem", "8rem"]}
|
||||
shrinkZero
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (data?.total_items === 0 && !isFiltered) {
|
||||
return (
|
||||
<SimpleEmptyState
|
||||
subtitle='Empieza cargando los artículos del catálogo'
|
||||
buttonText=''
|
||||
onButtonClick={() => navigate("/catalog/add")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable table={table} paginationOptions={{ visible: true }}>
|
||||
<DataTableToolbar table={table} />
|
||||
</DataTable>
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
client/src/app/dealers/components/index.ts
Normal file
1
client/src/app/dealers/components/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./DealerDataTable";
|
||||
77
client/src/app/dealers/components/useCatalogTableColumns.tsx
Normal file
77
client/src/app/dealers/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/dealers/hooks/index.ts
Normal file
1
client/src/app/dealers/hooks/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./useDealerList";
|
||||
39
client/src/app/dealers/hooks/useDealerList.tsx
Normal file
39
client/src/app/dealers/hooks/useDealerList.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
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 UseDealerListParams = {
|
||||
pagination: {
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
};
|
||||
searchTerm?: string;
|
||||
enabled?: boolean;
|
||||
queryOptions?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type UseDealerListResponse = UseListQueryResult<
|
||||
IListResponse_DTO<IListArticles_Response_DTO>,
|
||||
unknown
|
||||
>;
|
||||
|
||||
export const useDealerList = (params: UseDealerListParams): UseDealerListResponse => {
|
||||
const dataSource = useDataSource();
|
||||
const keys = useQueryKey();
|
||||
|
||||
const { pagination, searchTerm = undefined, enabled = true, queryOptions } = params;
|
||||
|
||||
return useList({
|
||||
queryKey: keys().data().resource("dealer").action("list").params(params).get(),
|
||||
queryFn: () => {
|
||||
return dataSource.getList({
|
||||
resource: "dealers",
|
||||
quickSearchTerm: searchTerm,
|
||||
pagination,
|
||||
});
|
||||
},
|
||||
enabled,
|
||||
queryOptions,
|
||||
});
|
||||
};
|
||||
@ -1,311 +1,10 @@
|
||||
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";
|
||||
import { DataTableProvider } from "@/lib/hooks";
|
||||
import { DealerDataTable } from "./components";
|
||||
|
||||
export const DealersList = () => {
|
||||
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'>
|
||||
<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>
|
||||
</>
|
||||
<DataTableProvider>
|
||||
<DealerDataTable />
|
||||
</DataTableProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -32,7 +32,7 @@ export function DataTableToolbar<TData>({
|
||||
<div className='flex items-center flex-1 space-x-2'>
|
||||
<Input
|
||||
key='global-filter'
|
||||
placeholder={t("catalog.list.global_filter_placeholder")}
|
||||
placeholder={t("common.filter_placeholder")}
|
||||
value={globalFilter}
|
||||
onChange={(event) => setGlobalFilter(String(event.target.value))}
|
||||
className='w-3/12 h-8 lg:w-6/12'
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
"go_to_prev_page": "Ir a la página anterior",
|
||||
"go_to_next_page": "Ir a la página siguiente",
|
||||
"go_to_last_page": "Ir a la última página",
|
||||
"filter_placeholder": "Escribe aquí para filtrar...",
|
||||
"reset_filter": "Quitar el filtro",
|
||||
"error": "Error"
|
||||
},
|
||||
@ -56,7 +57,6 @@
|
||||
"catalog": {
|
||||
"title": "Catálogo de artículos",
|
||||
"list": {
|
||||
"global_filter_placeholder": "Escribe aquí para filtrar los artículos...",
|
||||
"columns": {
|
||||
"description": "Descripción",
|
||||
"points": "Puntos",
|
||||
|
||||
14
shared/jest.config.js
Normal file
14
shared/jest.config.js
Normal file
@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
globals: {},
|
||||
moduleFileExtensions: ["ts", "js"],
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsconfig: "tsconfig.json",
|
||||
},
|
||||
],
|
||||
},
|
||||
testMatch: ["**/*.test.(ts|js)"],
|
||||
testEnvironment: "node",
|
||||
};
|
||||
@ -3,52 +3,61 @@ import { Quantity } from "./Quantity"; // Asegúrate de importar correctamente l
|
||||
describe("Quantity Value Object", () => {
|
||||
// Prueba la creación de una cantidad válida.
|
||||
it("Should create a valid quantity (number)", () => {
|
||||
const validQuantity = Quantity.create(5);
|
||||
const validQuantity = Quantity.create({ amount: 5 });
|
||||
|
||||
expect(validQuantity.isSuccess).toBe(true);
|
||||
expect(validQuantity.object.toNumber()).toBe(5);
|
||||
});
|
||||
|
||||
it("Should create a valid quantity (string)", () => {
|
||||
const validQuantity = Quantity.create("99");
|
||||
const validQuantity = Quantity.create({ amount: "99" });
|
||||
|
||||
expect(validQuantity.isSuccess).toBe(true);
|
||||
expect(validQuantity.object.toNumber()).toBe(99);
|
||||
});
|
||||
|
||||
it("Should create a valid quantity (null)", () => {
|
||||
const validQuantity = Quantity.create(null);
|
||||
const validQuantity = Quantity.create({ amount: null });
|
||||
|
||||
expect(validQuantity.isSuccess).toBe(true);
|
||||
expect(validQuantity.object.isNull).toBeTruthy();
|
||||
});
|
||||
|
||||
// Prueba la creación de una cantidad nula.
|
||||
it("Should create a valid null quantity", () => {
|
||||
const nullQuantity = Quantity.create(null);
|
||||
it("Should create a default quantity", () => {
|
||||
const nullQuantity = Quantity.create();
|
||||
|
||||
expect(nullQuantity.isSuccess).toBe(true);
|
||||
expect(nullQuantity.object.isNull()).toBe(true);
|
||||
expect(nullQuantity.object.amount).toBe(1);
|
||||
expect(nullQuantity.object.precision).toBe(0);
|
||||
});
|
||||
|
||||
// Prueba la creación de una cantidad válida a partir de una cadena.
|
||||
it("Should create a valid quantity from string", () => {
|
||||
const validQuantityFromString = Quantity.create("10");
|
||||
const validQuantityFromString = Quantity.create({ amount: "10" });
|
||||
|
||||
expect(validQuantityFromString.isSuccess).toBe(true);
|
||||
expect(validQuantityFromString.object.toNumber()).toBe(10);
|
||||
});
|
||||
|
||||
// Prueba la creación de una cantidad válida a partir de una cadena con decimales.
|
||||
it("Should create a valid quantity from string", () => {
|
||||
const validQuantityFromString = Quantity.create({ amount: "123456", precision: 2 });
|
||||
|
||||
expect(validQuantityFromString.isSuccess).toBe(true);
|
||||
expect(validQuantityFromString.object.toNumber()).toBe(1234.56);
|
||||
});
|
||||
|
||||
// Prueba la creación de una cantidad con una cadena no válida.
|
||||
it("Should fail to create quantity from invalid string", () => {
|
||||
const invalidQuantityFromString = Quantity.create("invalid");
|
||||
const invalidQuantityFromString = Quantity.create({ amount: "invalid" });
|
||||
|
||||
expect(invalidQuantityFromString.isFailure).toBe(true);
|
||||
});
|
||||
|
||||
// Prueba la conversión a número.
|
||||
it("Should convert to number", () => {
|
||||
const quantity = Quantity.create(7).object;
|
||||
const quantity = Quantity.create({ amount: 7 }).object;
|
||||
const result = quantity.toNumber();
|
||||
|
||||
expect(result).toBe(7);
|
||||
@ -56,25 +65,41 @@ describe("Quantity Value Object", () => {
|
||||
|
||||
// Prueba la conversión a cadena.
|
||||
it("Should convert to string", () => {
|
||||
const quantity = Quantity.create(15).object;
|
||||
const quantity = Quantity.create({ amount: 15 }).object;
|
||||
const result = quantity.toString();
|
||||
|
||||
expect(result).toBe("15");
|
||||
});
|
||||
|
||||
// Prueba la operación de incremento.
|
||||
it("Should increment quantity", () => {
|
||||
const quantity = Quantity.create(5).object;
|
||||
const incrementedQuantity = quantity.increment(3).object;
|
||||
it("Should increment", () => {
|
||||
const quantity = Quantity.create({ amount: 5 }).object;
|
||||
const incrementedQuantity = quantity.increment().object;
|
||||
|
||||
expect(incrementedQuantity.toNumber()).toBe(8);
|
||||
expect(incrementedQuantity.toNumber()).toBe(6);
|
||||
});
|
||||
|
||||
it("Should increment quantity", () => {
|
||||
const firstQ = Quantity.create({ amount: -5 }).object;
|
||||
const secountQ = Quantity.create({ amount: 105 }).object;
|
||||
const incrementedQuantity = firstQ.increment(secountQ).object;
|
||||
|
||||
expect(incrementedQuantity.toNumber()).toBe(100);
|
||||
});
|
||||
|
||||
// Prueba la operación de decremento.
|
||||
it("Should decrement", () => {
|
||||
const quantity = Quantity.create({ amount: 0 }).object;
|
||||
const decrementedQuantity = quantity.decrement().object;
|
||||
|
||||
expect(decrementedQuantity.toNumber()).toBe(-1);
|
||||
});
|
||||
|
||||
// Prueba la operación de decremento.
|
||||
it("Should decrement quantity", () => {
|
||||
const quantity = Quantity.create(10).object;
|
||||
const decrementedQuantity = quantity.decrement(4).object;
|
||||
const quantity = Quantity.create({ amount: 10 }).object;
|
||||
const decrementedQuantity = quantity.decrement(Quantity.create({ amount: 110 }).object).object;
|
||||
|
||||
expect(decrementedQuantity.toNumber()).toBe(6);
|
||||
expect(decrementedQuantity.toNumber()).toBe(-100);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,28 +1,41 @@
|
||||
import Joi from "joi";
|
||||
import { isNull } from "lodash";
|
||||
import { NullOr } from "../../../../utilities";
|
||||
import { RuleValidator } from "../RuleValidator";
|
||||
import {
|
||||
INullableValueObjectOptions,
|
||||
NullableValueObject,
|
||||
} from "./NullableValueObject";
|
||||
import { INullableValueObjectOptions, NullableValueObject } from "./NullableValueObject";
|
||||
import { Result } from "./Result";
|
||||
|
||||
export interface IQuantityOptions extends INullableValueObjectOptions {}
|
||||
|
||||
export class Quantity extends NullableValueObject<number> {
|
||||
protected static validate(
|
||||
value: NullOr<number | string>,
|
||||
options: IQuantityOptions = {},
|
||||
) {
|
||||
export interface IQuantityProps {
|
||||
amount: NullOr<number | string>;
|
||||
precision?: number;
|
||||
}
|
||||
|
||||
interface IQuantity {
|
||||
amount: NullOr<number>;
|
||||
precision: number;
|
||||
}
|
||||
|
||||
const defaultQuantityProps = {
|
||||
amount: 1,
|
||||
precision: 0,
|
||||
};
|
||||
|
||||
export class Quantity extends NullableValueObject<IQuantity> {
|
||||
private readonly _isNull: boolean;
|
||||
private readonly _options: IQuantityOptions;
|
||||
|
||||
protected static validate(value: NullOr<number | string>, options: IQuantityOptions = {}) {
|
||||
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(null);
|
||||
|
||||
const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label(
|
||||
options.label ? options.label : "quantity",
|
||||
options.label ? options.label : "quantity"
|
||||
);
|
||||
|
||||
const ruleString = RuleValidator.RULE_IS_TYPE_STRING.regex(
|
||||
/^[-]?\d+$/,
|
||||
).label(options.label ? options.label : "quantity");
|
||||
const ruleString = RuleValidator.RULE_IS_TYPE_STRING.regex(/^[-]?\d+$/).label(
|
||||
options.label ? options.label : "quantity"
|
||||
);
|
||||
|
||||
const rules = Joi.alternatives(ruleNull, ruleNumber, ruleString);
|
||||
|
||||
@ -30,68 +43,157 @@ export class Quantity extends NullableValueObject<number> {
|
||||
}
|
||||
|
||||
public static create(
|
||||
value: NullOr<number | string>,
|
||||
options: IQuantityOptions = {},
|
||||
props: IQuantityProps = defaultQuantityProps,
|
||||
options: IQuantityOptions = {}
|
||||
) {
|
||||
if (props === null) {
|
||||
throw new Error(`InvalidParams: props params is missing`);
|
||||
}
|
||||
|
||||
const { amount = defaultQuantityProps.amount, precision = defaultQuantityProps.precision } =
|
||||
props;
|
||||
|
||||
const _options = {
|
||||
label: "quantity",
|
||||
...options,
|
||||
};
|
||||
|
||||
const validationResult = Quantity.validate(value, _options);
|
||||
const validationResult = Quantity.validate(amount, _options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(validationResult.error);
|
||||
}
|
||||
|
||||
let _value: NullOr<number> = null;
|
||||
let _amount: NullOr<number> = Quantity.sanitize(validationResult.object);
|
||||
|
||||
if (typeof validationResult.object === "string") {
|
||||
_value = parseInt(validationResult.object, 10);
|
||||
} else {
|
||||
_value = validationResult.object;
|
||||
}
|
||||
const _props = {
|
||||
amount: isNull(_amount) ? 0 : _amount,
|
||||
precision,
|
||||
};
|
||||
|
||||
return Result.ok<Quantity>(new Quantity(_value));
|
||||
return Result.ok<Quantity>(new this(_props, isNull(_amount), options));
|
||||
}
|
||||
|
||||
private static sanitize(value: NullOr<number | string>): NullOr<number> {
|
||||
let _value: NullOr<number> = null;
|
||||
|
||||
if (typeof value === "string") {
|
||||
_value = parseInt(value, 10);
|
||||
} else {
|
||||
_value = value;
|
||||
}
|
||||
|
||||
return _value;
|
||||
}
|
||||
|
||||
constructor(quantity: IQuantity, isNull: boolean, options: IQuantityOptions) {
|
||||
super(quantity);
|
||||
this._isNull = Object.freeze(isNull);
|
||||
this._options = Object.freeze(options);
|
||||
}
|
||||
|
||||
get amount(): NullOr<number> {
|
||||
return this.isNull() ? null : Number(this.props?.amount);
|
||||
}
|
||||
|
||||
get precision(): number {
|
||||
return this.isNull() ? 0 : Number(this.props?.precision);
|
||||
}
|
||||
|
||||
public isEmpty = (): boolean => {
|
||||
return this.isNull();
|
||||
};
|
||||
|
||||
public isNull = (): boolean => {
|
||||
return this._isNull;
|
||||
};
|
||||
|
||||
public toNumber(): number {
|
||||
return this.isNull() ? 0 : Number(this.value);
|
||||
if (this.isNull()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const factor = Math.pow(10, this.precision);
|
||||
const amount = Number(this.amount) / factor;
|
||||
return Number(amount.toFixed(this.precision));
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.isNull() ? "" : String(this.value);
|
||||
return this.isNull() ? "" : String(this.toNumber());
|
||||
}
|
||||
|
||||
public toPrimitive(): number {
|
||||
return this.toNumber();
|
||||
}
|
||||
|
||||
public increment(amount: number = 1) {
|
||||
const validationResult = Quantity.validate(amount);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(validationResult.error);
|
||||
}
|
||||
|
||||
if (this.value === null) {
|
||||
return Quantity.create(amount);
|
||||
}
|
||||
|
||||
return Quantity.create(this.value + amount);
|
||||
public toPrimitives() {
|
||||
return this.toObject();
|
||||
}
|
||||
|
||||
public decrement(amount: number = 1) {
|
||||
const validationResult = Quantity.validate(amount);
|
||||
public toObject(): IQuantityProps {
|
||||
return {
|
||||
amount: this.amount,
|
||||
precision: this.precision,
|
||||
};
|
||||
}
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(validationResult.error);
|
||||
public hasSamePrecision(quantity: Quantity) {
|
||||
return this.precision === quantity.precision;
|
||||
}
|
||||
|
||||
public increment(anotherQuantity?: Quantity) {
|
||||
if (!anotherQuantity) {
|
||||
return Quantity.create(
|
||||
{
|
||||
amount: this.toNumber() + 1,
|
||||
precision: this.precision,
|
||||
},
|
||||
this._options
|
||||
);
|
||||
}
|
||||
|
||||
if (this.value === null) {
|
||||
return Quantity.create(amount);
|
||||
if (!this.hasSamePrecision(anotherQuantity)) {
|
||||
return Result.fail(Error("No se pueden sumar cantidades con diferentes precisiones."));
|
||||
}
|
||||
|
||||
return Quantity.create(this.value - amount);
|
||||
if (this.isNull()) {
|
||||
return Quantity.create(anotherQuantity.toObject());
|
||||
}
|
||||
|
||||
return Quantity.create(
|
||||
{
|
||||
amount: this.toNumber() + anotherQuantity.toNumber(),
|
||||
precision: this.precision,
|
||||
},
|
||||
this._options
|
||||
);
|
||||
}
|
||||
|
||||
public decrement(anotherQuantity?: Quantity) {
|
||||
if (!anotherQuantity) {
|
||||
return Quantity.create(
|
||||
{
|
||||
amount: this.toNumber() - 1,
|
||||
precision: this.precision,
|
||||
},
|
||||
this._options
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.hasSamePrecision(anotherQuantity)) {
|
||||
return Result.fail(Error("No se pueden restar cantidades con diferentes precisiones."));
|
||||
}
|
||||
|
||||
if (this.isNull()) {
|
||||
return Quantity.create(anotherQuantity.toObject());
|
||||
}
|
||||
|
||||
return Quantity.create(
|
||||
{
|
||||
amount: this.toNumber() - anotherQuantity.toNumber(),
|
||||
precision: this.precision,
|
||||
},
|
||||
this._options
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
export interface IListDealers_Response_DTO {
|
||||
id: string;
|
||||
name: string;
|
||||
language: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
"author": "Rodax Software <dev@rodax-software.com>",
|
||||
"license": "ISC",
|
||||
"scripts": {
|
||||
"clean": "rm -rf node_modules"
|
||||
"clean": "rm -rf node_modules",
|
||||
"test": "jest --verbose"
|
||||
},
|
||||
"dependencies": {
|
||||
"dinero.js": "^1.9.1",
|
||||
|
||||
81
shared/tsconfig.json
Normal file
81
shared/tsconfig.json
Normal file
@ -0,0 +1,81 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "ES2022" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"dom"
|
||||
] /* Specify library files to be included in the compilation. */,
|
||||
|
||||
"allowJs": false /* Allow javascript files to be compiled. */,
|
||||
"pretty": true,
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
"jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
"sourceMap": true /* Generates corresponding '.map' file. */,
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "../dist/" /* Redirect output structure to the directory. */,
|
||||
//"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
|
||||
// "composite": true, /* Enable project compilation */
|
||||
"removeComments": true /* Do not emit comments to output. */,
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"skipLibCheck": false /* Skip type checking of declaration files. */,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
|
||||
"strictNullChecks": true /* Enable strict null checks. */,
|
||||
"strictFunctionTypes": true /* Enable strict checking of function types. */,
|
||||
"strictPropertyInitialization": false /* Enable strict checking of property initialization in classes. */,
|
||||
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": false /* Report errors on unused locals. */,
|
||||
"noUnusedParameters": false /* Report errors on unused parameters. */,
|
||||
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
|
||||
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
|
||||
//"baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
|
||||
"paths": {
|
||||
/* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
"@/*": ["./src/*"],
|
||||
"@shared/*": ["../shared/lib/*"]
|
||||
},
|
||||
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [] /* List of folders to include type definitions from. */,
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
|
||||
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
|
||||
|
||||
/* Advanced Options */
|
||||
"resolveJsonModule": true /* Include modules imported with '.json' extension */,
|
||||
"suppressImplicitAnyIndexErrors": false
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/__tests__/*",
|
||||
"src/**/*.mock.*",
|
||||
"src/**/*.test.*",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user