.
This commit is contained in:
parent
ae0dabfca3
commit
981d70cffe
@ -1,4 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
$schema: "https://json.schemastore.org/eslintrc",
|
||||||
root: true,
|
root: true,
|
||||||
env: { browser: true, es2020: true },
|
env: { browser: true, es2020: true },
|
||||||
extends: [
|
extends: [
|
||||||
@ -12,5 +13,6 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
|
"react/no-unescaped-entities": "off",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,9 +6,8 @@ import { Suspense } from "react";
|
|||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import { Routes } from "./Routes";
|
import { Routes } from "./Routes";
|
||||||
import { LoadingOverlay, TailwindIndicator } from "./components";
|
import { LoadingOverlay, TailwindIndicator } from "./components";
|
||||||
import { createAxiosDataProvider } from "./lib/axios";
|
import { createAxiosAuthActions, createAxiosDataProvider } from "./lib/axios";
|
||||||
import { createAxiosAuthActions } from "./lib/axios/createAxiosAuthActions";
|
import { DataSourceProvider } from "./lib/hooks";
|
||||||
import { DataSourceProvider } from "./lib/hooks/useDataSource/DataSourceContext";
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@ -28,6 +27,7 @@ function App() {
|
|||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Suspense fallback={<LoadingOverlay />}>
|
<Suspense fallback={<LoadingOverlay />}>
|
||||||
<Routes />
|
<Routes />
|
||||||
|
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export const Routes = () => {
|
|||||||
element: <QuoteCreate />,
|
element: <QuoteCreate />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "edit",
|
path: "edit/:id",
|
||||||
element: <QuoteEdit />,
|
element: <QuoteEdit />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -109,11 +109,7 @@ export const Routes = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/logout",
|
path: "/logout",
|
||||||
element: (
|
element: <LogoutPage />,
|
||||||
<ProtectedRoute>
|
|
||||||
<LogoutPage />
|
|
||||||
</ProtectedRoute>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -24,7 +24,7 @@ export const CatalogDataTable = () => {
|
|||||||
searchTerm: globalFilter,
|
searchTerm: globalFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<IListArticles_Response_DTO, any>[]>(
|
const columns = useMemo<ColumnDef<IListArticles_Response_DTO>[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
id: "id" as const,
|
id: "id" as const,
|
||||||
@ -65,7 +65,7 @@ export const CatalogDataTable = () => {
|
|||||||
id: "retail_price" as const,
|
id: "retail_price" as const,
|
||||||
accessorKey: "retail_price",
|
accessorKey: "retail_price",
|
||||||
header: () => <div className='text-right'>{t("catalog.list.columns.retail_price")}</div>,
|
header: () => <div className='text-right'>{t("catalog.list.columns.retail_price")}</div>,
|
||||||
cell: ({ row }: { row: Row<any> }) => {
|
cell: ({ row }: { row: Row<IListArticles_Response_DTO> }) => {
|
||||||
const price = MoneyValue.create(row.original.retail_price).object;
|
const price = MoneyValue.create(row.original.retail_price).object;
|
||||||
return <div className='text-right'>{price.toFormat()}</div>;
|
return <div className='text-right'>{price.toFormat()}</div>;
|
||||||
},
|
},
|
||||||
@ -113,10 +113,8 @@ export const CatalogDataTable = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DataTable table={table} paginationOptions={{ visible: true }} title='Catálogo'>
|
||||||
<DataTable table={table} paginationOptions={{ visible: true }}>
|
<DataTableToolbar table={table} />
|
||||||
<DataTableToolbar table={table} />
|
</DataTable>
|
||||||
</DataTable>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import {
|
|||||||
PaginationItem,
|
PaginationItem,
|
||||||
Progress,
|
Progress,
|
||||||
Separator,
|
Separator,
|
||||||
|
Skeleton,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
@ -44,20 +45,24 @@ import {
|
|||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@/ui";
|
} from "@/ui";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export const DashboardPage = () => {
|
export const DashboardPage = () => {
|
||||||
const { data, status } = useGetIdentity();
|
const navigate = useNavigate();
|
||||||
|
const { data: userIdentity, status } = useGetIdentity();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<LayoutHeader />
|
<LayoutHeader />
|
||||||
<LayoutContent>
|
<LayoutContent>
|
||||||
{status === "success" && (
|
{status === "success" ? (
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<h1 className='text-lg font-semibold md:text-2xl'>{`${t("dashboard.welcome")}, ${
|
<h1 className='text-lg font-semibold md:text-2xl'>{`${t("dashboard.welcome")}, ${
|
||||||
data?.name
|
userIdentity?.name
|
||||||
}`}</h1>
|
}`}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Skeleton className='w-[100px] h-[20px] rounded-full' />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<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 flex-1 gap-4 p-4 sm:px-6 sm:py-0 md:gap-8 lg:grid-cols-3 xl:grid-cols-3'>
|
||||||
@ -72,7 +77,9 @@ export const DashboardPage = () => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<Button>Crear nueva cotización</Button>
|
<Button onClick={() => navigate("/quotes/add")}>
|
||||||
|
{t("quotes.create.title")}
|
||||||
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
<Card x-chunk='dashboard-05-chunk-1'>
|
<Card x-chunk='dashboard-05-chunk-1'>
|
||||||
|
|||||||
134
client/src/app/quotes/components/CatalogPickerDataTable.tsx
Normal file
134
client/src/app/quotes/components/CatalogPickerDataTable.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { Card, CardContent } from "@/ui";
|
||||||
|
|
||||||
|
import { DataTableSkeleton, ErrorOverlay, SimpleEmptyState } from "@/components";
|
||||||
|
|
||||||
|
import { useCatalogList } from "@/app/catalog/hooks";
|
||||||
|
import { DataTable } from "@/components";
|
||||||
|
import { DataTableToolbar } from "@/components/DataTable/DataTableToolbar";
|
||||||
|
import { useDataTable, useDataTableContext } from "@/lib/hooks";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { IListArticles_Response_DTO, MoneyValue } from "@shared/contexts";
|
||||||
|
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export const CatalogPickerDataTable = ({ onClick }: { onClick: (data: unknown) => void }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { pagination, globalFilter, isFiltered } = useDataTableContext();
|
||||||
|
|
||||||
|
const { data, isPending, isError, error } = useCatalogList({
|
||||||
|
pagination: {
|
||||||
|
pageIndex: pagination.pageIndex,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
},
|
||||||
|
searchTerm: globalFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = useMemo<ColumnDef<IListArticles_Response_DTO>[]>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "description" as const,
|
||||||
|
accessorKey: "description",
|
||||||
|
enableResizing: false,
|
||||||
|
header: () => null,
|
||||||
|
cell: ({
|
||||||
|
row,
|
||||||
|
renderValue,
|
||||||
|
}: {
|
||||||
|
row: Row<IListArticles_Response_DTO>;
|
||||||
|
renderValue: () => any;
|
||||||
|
}) => {
|
||||||
|
const price = MoneyValue.create(row.original.retail_price).object;
|
||||||
|
const points = row.original.points;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={row.id}
|
||||||
|
className={cn("rounded-lg border p-3 transition-all hover:bg-accent w-full", "")}
|
||||||
|
onClick={
|
||||||
|
(event) => {
|
||||||
|
console.log("hola");
|
||||||
|
event.preventDefault();
|
||||||
|
onClick && onClick(row.original);
|
||||||
|
}
|
||||||
|
/*setMail({
|
||||||
|
...mail,
|
||||||
|
selected: item.id,
|
||||||
|
})*/
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<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'>
|
||||||
|
{renderValue()}
|
||||||
|
</div>
|
||||||
|
<div className='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")}:
|
||||||
|
</dt>
|
||||||
|
<dd className='text-xs font-semibold'>{points}</dd>
|
||||||
|
</dl>
|
||||||
|
<dl className='flex flex-row justify-end space-x-1'>
|
||||||
|
<dt className='text-xs font-medium text-accent-foreground/75'>
|
||||||
|
{t("catalog.list.columns.retail_price")}:
|
||||||
|
</dt>
|
||||||
|
<dt className='text-xs font-semibold'>{price.toFormat()}</dt>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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}
|
||||||
|
headerOptions={{ visible: false }}
|
||||||
|
paginationOptions={{ visible: true }}
|
||||||
|
title='Catálogo'
|
||||||
|
rowClassName='border-b-0'
|
||||||
|
cellClassName='px-0'
|
||||||
|
>
|
||||||
|
<DataTableToolbar fullWidthFilter={true} table={table} />
|
||||||
|
</DataTable>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -125,6 +125,7 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
|
|||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
|
const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||||
const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]);
|
const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]);
|
||||||
|
|
||||||
@ -145,8 +146,8 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
|
|||||||
onRowSelectionChange: setRowSelection,
|
onRowSelectionChange: setRowSelection,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getRowId: (originalRow: unknown) => originalRow?.id,
|
getRowId: (originalRow: unknown) => originalRow?.id,
|
||||||
debugHeaders: true,
|
debugHeaders: false,
|
||||||
debugColumns: true,
|
debugColumns: false,
|
||||||
meta: {
|
meta: {
|
||||||
insertItem: (rowIndex: number, data: object = {}) => {
|
insertItem: (rowIndex: number, data: object = {}) => {
|
||||||
actions.insert(rowIndex, data, { shouldFocus: true });
|
actions.insert(rowIndex, data, { shouldFocus: true });
|
||||||
@ -263,16 +264,43 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
|
|||||||
const hadleNewItem = useCallback(() => {
|
const hadleNewItem = useCallback(() => {
|
||||||
actions.append([
|
actions.append([
|
||||||
{
|
{
|
||||||
description: "a",
|
quantity: { amount: "123" },
|
||||||
|
description: "aaaa",
|
||||||
|
retail_price: {
|
||||||
|
amount: "10000",
|
||||||
|
precision: 4,
|
||||||
|
currency: "EUR",
|
||||||
|
},
|
||||||
|
discount: {
|
||||||
|
amount: 35,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "b",
|
quantity: {
|
||||||
|
amount: "2",
|
||||||
|
},
|
||||||
|
description: "bbbb",
|
||||||
|
discount: {
|
||||||
|
amount: 55,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "c",
|
quantity: {
|
||||||
|
amount: "3",
|
||||||
|
},
|
||||||
|
description: "cccc",
|
||||||
|
discount: {
|
||||||
|
amount: 75,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "d",
|
quantity: {
|
||||||
|
amount: "4",
|
||||||
|
},
|
||||||
|
description: "dddd",
|
||||||
|
discount: {
|
||||||
|
amount: 10,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}, [actions]);
|
}, [actions]);
|
||||||
|
|||||||
@ -1,20 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
ButtonGroup,
|
|
||||||
CancelButton,
|
|
||||||
FormGroup,
|
|
||||||
FormMoneyField,
|
FormMoneyField,
|
||||||
|
FormPercentageField,
|
||||||
|
FormQuantityField,
|
||||||
FormTextAreaField,
|
FormTextAreaField,
|
||||||
FormTextField,
|
|
||||||
SubmitButton,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { Input } from "@/ui";
|
import { DataTableProvider } from "@/lib/hooks";
|
||||||
import { t } from "i18next";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/ui";
|
||||||
|
import { Quantity } from "@shared/contexts";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||||
import { useDetailColumns } from "../../hooks";
|
import { useDetailColumns } from "../../hooks";
|
||||||
|
import { CatalogPickerDataTable } from "../CatalogPickerDataTable";
|
||||||
import { SortableDataTable } from "../SortableDataTable";
|
import { SortableDataTable } from "../SortableDataTable";
|
||||||
|
|
||||||
export const QuoteDetailsCardEditor = () => {
|
export const QuoteDetailsCardEditor = () => {
|
||||||
const { control, register, formState } = useFormContext();
|
const { control, register, watch, getValues, setValue } = useFormContext();
|
||||||
|
|
||||||
const { fields, ...fieldActions } = useFieldArray({
|
const { fields, ...fieldActions } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
@ -39,27 +40,16 @@ export const QuoteDetailsCardEditor = () => {
|
|||||||
accessorKey: "quantity",
|
accessorKey: "quantity",
|
||||||
header: "quantity",
|
header: "quantity",
|
||||||
size: 5,
|
size: 5,
|
||||||
cell: ({ row: { index }, column: { id } }) => {
|
cell: ({ row: { index } }) => {
|
||||||
return (
|
return <FormQuantityField {...register(`items.${index}.quantity`)} />;
|
||||||
<FormTextField
|
|
||||||
type='number'
|
|
||||||
control={control}
|
|
||||||
{...register(`items.${index}.quantity`)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "description" as const,
|
id: "description" as const,
|
||||||
accessorKey: "description",
|
accessorKey: "description",
|
||||||
cell: ({ row: { index }, column: { id } }) => {
|
header: "description",
|
||||||
return (
|
cell: ({ row: { index } }) => {
|
||||||
<FormTextAreaField
|
return <FormTextAreaField autoSize {...register(`items.${index}.description`)} />;
|
||||||
autoSize
|
|
||||||
control={control}
|
|
||||||
{...register(`items.${index}.description`)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -69,7 +59,7 @@ export const QuoteDetailsCardEditor = () => {
|
|||||||
header: "retail_price",
|
header: "retail_price",
|
||||||
size: 10,
|
size: 10,
|
||||||
cell: ({ row: { index }, column: { id } }) => {
|
cell: ({ row: { index }, column: { id } }) => {
|
||||||
return <Input key={id} {...register(`items.${index}.retail_price`)} />;
|
return <FormMoneyField {...register(`items.${index}.retail_price`)} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -78,7 +68,7 @@ export const QuoteDetailsCardEditor = () => {
|
|||||||
header: "price",
|
header: "price",
|
||||||
size: 10,
|
size: 10,
|
||||||
cell: ({ row: { index }, column: { id } }) => {
|
cell: ({ row: { index }, column: { id } }) => {
|
||||||
return <FormMoneyField control={control} {...register(`items.${index}.price`)} />;
|
return <FormMoneyField {...register(`items.${index}.price`)} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -87,7 +77,7 @@ export const QuoteDetailsCardEditor = () => {
|
|||||||
header: "discount",
|
header: "discount",
|
||||||
size: 5,
|
size: 5,
|
||||||
cell: ({ row: { index }, column: { id } }) => {
|
cell: ({ row: { index }, column: { id } }) => {
|
||||||
return <FormMoneyField control={control} {...register(`items.${index}.discount`)} />;
|
return <FormPercentageField {...register(`items.${index}.discount`)} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -96,7 +86,7 @@ export const QuoteDetailsCardEditor = () => {
|
|||||||
header: "total",
|
header: "total",
|
||||||
size: 10,
|
size: 10,
|
||||||
cell: ({ row: { index }, column: { id } }) => {
|
cell: ({ row: { index }, column: { id } }) => {
|
||||||
return <FormMoneyField control={control} {...register(`items.${index}.total`)} />;
|
return <FormMoneyField {...register(`items.${index}.total`)} />;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -135,24 +125,54 @@ export const QuoteDetailsCardEditor = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleInsertArticle = useCallback(
|
||||||
|
(newArticle) => {
|
||||||
|
console.log(newArticle);
|
||||||
|
|
||||||
|
fieldActions.append({
|
||||||
|
...newArticle,
|
||||||
|
quantity: {
|
||||||
|
amount: 1,
|
||||||
|
precision: Quantity.DEFAULT_PRECISION,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[fieldActions]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
|
||||||
|
const defaultLayout = [265, 440, 655];
|
||||||
|
const navCollapsedSize = 4;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<ResizablePanelGroup
|
||||||
title={t("quotes.create.tabs.items.title")}
|
direction='horizontal'
|
||||||
description={t("quotes.create.tabs.items.desc")}
|
autoSaveId='uecko.quotes.details_layout'
|
||||||
actions={
|
className='items-stretch h-full'
|
||||||
<ButtonGroup className='md:hidden'>
|
|
||||||
<CancelButton onClick={() => null} size='sm' />
|
|
||||||
<SubmitButton
|
|
||||||
disabled={!formState.isDirty || formState.isSubmitting || formState.isLoading}
|
|
||||||
label='Guardar'
|
|
||||||
size='sm'
|
|
||||||
/>
|
|
||||||
</ButtonGroup>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className='gap-0'>
|
<ResizablePanel
|
||||||
|
defaultSize={defaultLayout[0]}
|
||||||
|
collapsedSize={navCollapsedSize}
|
||||||
|
collapsible={true}
|
||||||
|
minSize={50}
|
||||||
|
maxSize={90}
|
||||||
|
onCollapse={() => {
|
||||||
|
setIsCollapsed(true);
|
||||||
|
}}
|
||||||
|
onExpand={() => {
|
||||||
|
setIsCollapsed(false);
|
||||||
|
}}
|
||||||
|
className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")}
|
||||||
|
>
|
||||||
<SortableDataTable actions={fieldActions} columns={columns} data={fields} />
|
<SortableDataTable actions={fieldActions} columns={columns} data={fields} />
|
||||||
</div>
|
</ResizablePanel>
|
||||||
</FormGroup>
|
<ResizableHandle withHandle className='mx-3' />
|
||||||
|
<ResizablePanel defaultSize={defaultLayout[1]} minSize={10}>
|
||||||
|
<DataTableProvider syncWithLocation={false}>
|
||||||
|
<CatalogPickerDataTable onClick={handleInsertArticle} />
|
||||||
|
</DataTableProvider>
|
||||||
|
</ResizablePanel>
|
||||||
|
</ResizablePanelGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,38 +9,21 @@ import { t } from "i18next";
|
|||||||
import { ChevronLeft } from "lucide-react";
|
import { ChevronLeft } from "lucide-react";
|
||||||
|
|
||||||
import { SubmitButton } from "@/components";
|
import { SubmitButton } from "@/components";
|
||||||
|
import { useWarnAboutChange } from "@/lib/hooks";
|
||||||
import { Button, Form } from "@/ui";
|
import { Button, Form } from "@/ui";
|
||||||
|
import { ICreateQuote_Request_DTO } from "@shared/contexts";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { FieldErrors, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
|
import { FieldErrors, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useQuotes } from "./hooks";
|
import { useQuotes } from "./hooks";
|
||||||
|
|
||||||
type QuoteDataForm = {
|
interface QuoteDataForm extends ICreateQuote_Request_DTO {}
|
||||||
id: string;
|
|
||||||
status: string;
|
|
||||||
date: string;
|
|
||||||
reference: string;
|
|
||||||
customer_information: string;
|
|
||||||
lang_code: string;
|
|
||||||
currency_code: string;
|
|
||||||
payment_method: string;
|
|
||||||
notes: string;
|
|
||||||
validity: string;
|
|
||||||
items: any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/*type QuoteCreateProps = {
|
|
||||||
isOverModal?: boolean;
|
|
||||||
};*/
|
|
||||||
|
|
||||||
export const QuoteCreate = () => {
|
export const QuoteCreate = () => {
|
||||||
//const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
//const { data: userIdentity } = useGetIdentity();
|
|
||||||
//console.log(userIdentity);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { useMutation } = useQuotes();
|
const { setWarnWhen } = useWarnAboutChange();
|
||||||
const { mutate } = useMutation();
|
const { useCreate } = useQuotes();
|
||||||
|
const { mutate } = useCreate();
|
||||||
|
|
||||||
const form = useForm<QuoteDataForm>({
|
const form = useForm<QuoteDataForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -50,11 +33,23 @@ export const QuoteCreate = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { watch, handleSubmit } = form;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = watch((values: any, { type }: { type?: any }) => {
|
||||||
|
if (type === "change") {
|
||||||
|
// Hay cambios en el formulario
|
||||||
|
//setWarnWhen(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [watch, setWarnWhen]);
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<QuoteDataForm> = async (formData) => {
|
const onSubmit: SubmitHandler<QuoteDataForm> = async (formData) => {
|
||||||
alert(JSON.stringify(formData));
|
console.log(JSON.stringify(formData));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//setLoading(true);
|
setWarnWhen(false);
|
||||||
mutate(formData, {
|
mutate(formData, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
navigate(`/quotes/edit/${data.id}`, { relative: "path", replace: true });
|
navigate(`/quotes/edit/${data.id}`, { relative: "path", replace: true });
|
||||||
@ -73,10 +68,15 @@ export const QuoteCreate = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit, onErrors)}>
|
<form onSubmit={handleSubmit(onSubmit, onErrors)}>
|
||||||
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>
|
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<Button variant='outline' size='icon' className='h-7 w-7'>
|
<Button
|
||||||
|
variant='outline'
|
||||||
|
size='icon'
|
||||||
|
className='h-7 w-7'
|
||||||
|
onClick={() => navigate("/quotes")}
|
||||||
|
>
|
||||||
<ChevronLeft className='w-4 h-4' />
|
<ChevronLeft className='w-4 h-4' />
|
||||||
<span className='sr-only'>{t("quotes.common.back")}</span>
|
<span className='sr-only'>{t("quotes.common.back")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
@ -85,7 +85,7 @@ export const QuoteCreate = () => {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid max-w-lg gap-6'>
|
<div className='grid w-6/12 gap-6 mx-auto'>
|
||||||
<FormTextField
|
<FormTextField
|
||||||
className='row-span-2'
|
className='row-span-2'
|
||||||
name='reference'
|
name='reference'
|
||||||
@ -112,12 +112,17 @@ export const QuoteCreate = () => {
|
|||||||
description={t("quotes.create.form_fields.customer_information.desc")}
|
description={t("quotes.create.form_fields.customer_information.desc")}
|
||||||
placeholder={t("quotes.create.form_fields.customer_information.placeholder")}
|
placeholder={t("quotes.create.form_fields.customer_information.placeholder")}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex items-center justify-start gap-2'>
|
<div className='flex items-center justify-around gap-2'>
|
||||||
<BackHistoryButton size='sm' label={t("quotes.create.buttons.discard")} url='/quotes' />
|
<BackHistoryButton
|
||||||
|
size='sm'
|
||||||
|
variant={"outline"}
|
||||||
|
label={t("quotes.create.buttons.discard")}
|
||||||
|
url='/quotes'
|
||||||
|
/>
|
||||||
|
|
||||||
<SubmitButton size='sm' label={t("common.continue")}></SubmitButton>
|
<SubmitButton size='sm' label={t("common.continue")}></SubmitButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -1,21 +1,23 @@
|
|||||||
import { ChevronLeft } from "lucide-react";
|
import { FormMoneyField, LoadingOverlay, SubmitButton } from "@/components";
|
||||||
|
import { calculateItemTotals } from "@/lib/calc";
|
||||||
import { SubmitButton } from "@/components";
|
|
||||||
import { useGetIdentity } from "@/lib/hooks";
|
import { useGetIdentity } from "@/lib/hooks";
|
||||||
|
import { useUrlId } from "@/lib/hooks/useUrlId";
|
||||||
import { Badge, Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui";
|
import { Badge, Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui";
|
||||||
|
import { IUpdateQuote_Request_DTO, MoneyValue } from "@shared/contexts";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useState } from "react";
|
import { ChevronLeftIcon } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
import {
|
import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors";
|
||||||
QuoteDetailsCardEditor,
|
|
||||||
QuoteDocumentsCardEditor,
|
|
||||||
QuoteGeneralCardEditor,
|
|
||||||
} from "./components/editors";
|
|
||||||
import { useQuotes } from "./hooks";
|
import { useQuotes } from "./hooks";
|
||||||
|
|
||||||
type QuoteDataForm = {
|
// simple typesafe helperfunction
|
||||||
id: string;
|
type EndsWith<T, b extends string> = T extends `${infer f}${b}` ? T : never;
|
||||||
status: string;
|
const endsWith = <T extends string, b extends string>(str: T, prefix: b): str is EndsWith<T, b> =>
|
||||||
|
str.endsWith(prefix);
|
||||||
|
|
||||||
|
interface QuoteDataForm extends IUpdateQuote_Request_DTO {
|
||||||
|
/*status: string;
|
||||||
date: string;
|
date: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
customer_information: string;
|
customer_information: string;
|
||||||
@ -24,23 +26,31 @@ type QuoteDataForm = {
|
|||||||
payment_method: string;
|
payment_method: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
validity: string;
|
validity: string;
|
||||||
items: any[];
|
discount: IPercentage;
|
||||||
};
|
|
||||||
|
|
||||||
type QuoteCreateProps = {
|
subtotal: IMoney;
|
||||||
isOverModal?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QuoteEdit = ({ isOverModal }: QuoteCreateProps) => {
|
items: {
|
||||||
|
quantity: IQuantity;
|
||||||
|
description: string;
|
||||||
|
retail_price: IMoney;
|
||||||
|
price: IMoney;
|
||||||
|
discount: IPercentage;
|
||||||
|
total: IMoney;
|
||||||
|
}[];*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
export const QuoteEdit = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const quoteId = useUrlId();
|
||||||
|
|
||||||
const { data: userIdentity } = useGetIdentity();
|
const { data: userIdentity } = useGetIdentity();
|
||||||
console.log(userIdentity);
|
|
||||||
|
|
||||||
const { useQuery, useMutation } = useQuotes();
|
const { useOne, useUpdate } = useQuotes();
|
||||||
|
|
||||||
const { data } = useQuery;
|
const { data, status } = useOne(quoteId);
|
||||||
const { mutate } = useMutation;
|
const { mutate } = useUpdate(quoteId);
|
||||||
|
|
||||||
const form = useForm<QuoteDataForm>({
|
const form = useForm<QuoteDataForm>({
|
||||||
mode: "onBlur",
|
mode: "onBlur",
|
||||||
@ -54,53 +64,119 @@ export const QuoteEdit = ({ isOverModal }: QuoteCreateProps) => {
|
|||||||
payment_method: "",
|
payment_method: "",
|
||||||
notes: "",
|
notes: "",
|
||||||
validity: "",
|
validity: "",
|
||||||
|
subtotal: "",
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<QuoteDataForm> = async (data) => {
|
const onSubmit: SubmitHandler<QuoteDataForm> = async (data) => {
|
||||||
alert(JSON.stringify(data));
|
console.debug(JSON.stringify(data));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
data.currency_code = "EUR";
|
// Transformación del form -> typo de request
|
||||||
data.lang_code = String(userIdentity?.language);
|
mutate(data, {
|
||||||
|
onError: (error) => {
|
||||||
mutate(data);
|
alert(error);
|
||||||
|
},
|
||||||
|
//onSettled: () => {},
|
||||||
|
onSuccess: () => {
|
||||||
|
alert("guardado");
|
||||||
|
},
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { watch, getValues, setValue } = form;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = watch((_, { name, type }) => {
|
||||||
|
const value = getValues();
|
||||||
|
|
||||||
|
console.debug({ name, type });
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
if (name === "items") {
|
||||||
|
const { items } = value;
|
||||||
|
let quoteSubtotal = MoneyValue.create().object;
|
||||||
|
|
||||||
|
// Recálculo líneas
|
||||||
|
items.map((item, index) => {
|
||||||
|
const itemTotals = calculateItemTotals(item);
|
||||||
|
quoteSubtotal = quoteSubtotal.add(itemTotals.total);
|
||||||
|
|
||||||
|
setValue(`items.${index}.price`, itemTotals.price.toObject());
|
||||||
|
setValue(`items.${index}.total`, itemTotals.total.toObject());
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(quoteSubtotal.toFormat());
|
||||||
|
|
||||||
|
// Recálculo completo
|
||||||
|
setValue("subtotal", quoteSubtotal.toObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
endsWith(name, "quantity") ||
|
||||||
|
endsWith(name, "retail_price") ||
|
||||||
|
endsWith(name, "discount")
|
||||||
|
) {
|
||||||
|
const { items } = value;
|
||||||
|
const [, indexString, fieldName] = String(name).split(".");
|
||||||
|
const index = parseInt(indexString);
|
||||||
|
|
||||||
|
const itemTotals = calculateItemTotals(items[index]);
|
||||||
|
|
||||||
|
setValue(`items.${index}.price`, itemTotals.price.toObject());
|
||||||
|
setValue(`items.${index}.total`, itemTotals.total.toObject());
|
||||||
|
|
||||||
|
// Recálculo completo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, [watch, getValues, setValue]);
|
||||||
|
|
||||||
|
if (status !== "success") {
|
||||||
|
return <LoadingOverlay />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>
|
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<Button variant='outline' size='icon' className='h-7 w-7'>
|
<Button variant='outline' size='icon' className='h-7 w-7'>
|
||||||
<ChevronLeft className='w-4 h-4' />
|
<ChevronLeftIcon className='w-4 h-4' />
|
||||||
<span className='sr-only'>{t("quotes.common.back")}</span>
|
<span className='sr-only'>{t("quotes.common.back")}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<h1 className='flex-1 text-xl font-semibold tracking-tight shrink-0 whitespace-nowrap sm:grow-0'>
|
<h1 className='flex-1 text-xl font-semibold tracking-tight shrink-0 whitespace-nowrap sm:grow-0'>
|
||||||
{t("quotes.create.title")}
|
{t("quotes.edit.title")}
|
||||||
</h1>
|
</h1>
|
||||||
<Badge variant='default' className='ml-auto sm:ml-0'>
|
<Badge variant='default' className='ml-auto sm:ml-0'>
|
||||||
{t("quotes.status.draft")}
|
{data.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className='items-center hidden gap-2 md:ml-auto md:flex'>
|
<div className='items-center hidden gap-2 md:ml-auto md:flex'>
|
||||||
<Button variant='outline' size='sm'>
|
<Button variant='outline' size='sm'>
|
||||||
{t("quotes.create.buttons.discard")}
|
{t("common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<SubmitButton variant={form.formState.isDirty ? "default" : "outline"} size='sm'>
|
<SubmitButton variant={form.formState.isDirty ? "default" : "outline"} size='sm'>
|
||||||
{t("quotes.create.buttons.save_quote")}
|
{t("common.save")}
|
||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tabs defaultValue='general' className='space-y-4'>
|
|
||||||
|
<FormMoneyField
|
||||||
|
label={"subtotal"}
|
||||||
|
disabled={form.formState.disabled}
|
||||||
|
{...form.register("subtotal")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabs defaultValue='items' className='space-y-4'>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value='general'>{t("quotes.create.tabs.general")}</TabsTrigger>
|
<TabsTrigger value='general'>{t("quotes.create.tabs.general")}</TabsTrigger>
|
||||||
<TabsTrigger value='items'>{t("quotes.create.tabs.items")}</TabsTrigger>
|
<TabsTrigger value='items'>{t("quotes.create.tabs.items")}</TabsTrigger>
|
||||||
<TabsTrigger value='documents'>{t("quotes.create.tabs.documents")}</TabsTrigger>
|
|
||||||
<TabsTrigger value='history'>{t("quotes.create.tabs.history")}</TabsTrigger>
|
<TabsTrigger value='history'>{t("quotes.create.tabs.history")}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value='general'>
|
<TabsContent value='general'>
|
||||||
@ -110,9 +186,6 @@ export const QuoteEdit = ({ isOverModal }: QuoteCreateProps) => {
|
|||||||
<QuoteDetailsCardEditor />
|
<QuoteDetailsCardEditor />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value='documents'>
|
|
||||||
<QuoteDocumentsCardEditor />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value='history'></TabsContent>
|
<TabsContent value='history'></TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<div className='flex items-center justify-center gap-2 md:hidden'>
|
<div className='flex items-center justify-center gap-2 md:hidden'>
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import {
|
|||||||
ICreateQuote_Request_DTO,
|
ICreateQuote_Request_DTO,
|
||||||
ICreateQuote_Response_DTO,
|
ICreateQuote_Response_DTO,
|
||||||
IGetQuote_Response_DTO,
|
IGetQuote_Response_DTO,
|
||||||
|
IUpdateQuote_Request_DTO,
|
||||||
|
IUpdateQuote_Response_DTO,
|
||||||
UniqueID,
|
UniqueID,
|
||||||
} from "@shared/contexts";
|
} from "@shared/contexts";
|
||||||
|
|
||||||
@ -14,22 +16,23 @@ export type UseQuotesGetParamsType = {
|
|||||||
queryOptions?: Record<string, unknown>;
|
queryOptions?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useQuotes = (params?: UseQuotesGetParamsType) => {
|
export const useQuotes = () => {
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const keys = useQueryKey();
|
const keys = useQueryKey();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
useQuery: () =>
|
useOne: (id?: string, params?: UseQuotesGetParamsType) =>
|
||||||
useOne<IGetQuote_Response_DTO>({
|
useOne<IGetQuote_Response_DTO>({
|
||||||
queryKey: keys().data().resource("quotes").action("one").id("").params().get(),
|
queryKey: keys().data().resource("quotes").action("one").id("").params().get(),
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
dataSource.getOne({
|
dataSource.getOne({
|
||||||
resource: "quotes",
|
resource: "quotes",
|
||||||
id: "",
|
id: String(id),
|
||||||
}),
|
}),
|
||||||
|
enabled: !!id,
|
||||||
...params,
|
...params,
|
||||||
}),
|
}),
|
||||||
useMutation: () =>
|
useCreate: () =>
|
||||||
useSave<ICreateQuote_Response_DTO, TDataSourceError, ICreateQuote_Request_DTO>({
|
useSave<ICreateQuote_Response_DTO, TDataSourceError, ICreateQuote_Request_DTO>({
|
||||||
mutationKey: keys().data().resource("quotes").action("one").id("").params().get(),
|
mutationKey: keys().data().resource("quotes").action("one").id("").params().get(),
|
||||||
mutationFn: (data) => {
|
mutationFn: (data) => {
|
||||||
@ -50,5 +53,17 @@ export const useQuotes = (params?: UseQuotesGetParamsType) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
useUpdate: (id: string) =>
|
||||||
|
useSave<IUpdateQuote_Response_DTO, TDataSourceError, IUpdateQuote_Request_DTO>({
|
||||||
|
mutationKey: keys().data().resource("quotes").action("one").id(id).params().get(),
|
||||||
|
mutationFn: (data) => {
|
||||||
|
return dataSource.updateOne({
|
||||||
|
resource: "quotes",
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
import { useOne } from '@/lib/hooks/useDataSource';
|
|
||||||
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 ISettingsActions {
|
|
||||||
getSettings: (
|
|
||||||
dataSource: IDataSource,
|
|
||||||
): Promise<IGetProfileResponse_DTO> => {
|
|
||||||
|
|
||||||
return useOne(
|
|
||||||
|
|
||||||
)
|
|
||||||
return dataProvider.getList({
|
|
||||||
resource: "invoices",
|
|
||||||
quickSearchTerm,
|
|
||||||
pagination,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,14 @@
|
|||||||
import { ColumnDef, Table as ReactTable, flexRender } from "@tanstack/react-table";
|
import { ColumnDef, Table as ReactTable, flexRender } from "@tanstack/react-table";
|
||||||
|
import { PropsWithChildren, ReactNode } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
Separator,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCaption,
|
TableCaption,
|
||||||
@ -8,11 +16,9 @@ import {
|
|||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/ui/table";
|
} from "@/ui";
|
||||||
import { PropsWithChildren, ReactNode } from "react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader } from "@/ui";
|
|
||||||
import { DataTableColumnHeader } from "./DataTableColumnHeader";
|
import { DataTableColumnHeader } from "./DataTableColumnHeader";
|
||||||
import { DataTablePagination, DataTablePaginationProps } from "./DataTablePagination";
|
import { DataTablePagination, DataTablePaginationProps } from "./DataTablePagination";
|
||||||
|
|
||||||
@ -23,38 +29,58 @@ export type DataTablePaginationOptionsProps<TData> = Pick<
|
|||||||
"visible"
|
"visible"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type DataTableHeaderOptionsProps = {
|
||||||
|
visible: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type DataTableProps<TData> = PropsWithChildren<{
|
export type DataTableProps<TData> = PropsWithChildren<{
|
||||||
table: ReactTable<TData>;
|
table: ReactTable<TData>;
|
||||||
|
title?: ReactNode;
|
||||||
|
description?: ReactNode;
|
||||||
caption?: ReactNode;
|
caption?: ReactNode;
|
||||||
paginationOptions?: DataTablePaginationOptionsProps<TData>;
|
paginationOptions?: DataTablePaginationOptionsProps<TData>;
|
||||||
|
headerOptions?: DataTableHeaderOptionsProps;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
rowClassName?: string;
|
||||||
|
cellClassName?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export function DataTable<TData>({
|
export function DataTable<TData>({
|
||||||
table,
|
table,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
caption,
|
caption,
|
||||||
paginationOptions,
|
paginationOptions,
|
||||||
|
headerOptions = { visible: true },
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
...props
|
rowClassName,
|
||||||
|
cellClassName,
|
||||||
}: DataTableProps<TData>) {
|
}: DataTableProps<TData>) {
|
||||||
|
const headerVisible = headerOptions?.visible;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Card className={className}>
|
||||||
<Card>
|
{(title || description) && (
|
||||||
<CardHeader className='pb-0'>
|
<CardHeader className='pb-0'>
|
||||||
<CardDescription
|
<CardTitle>{title}</CardTitle>
|
||||||
className={cn("w-full space-y-2.5 overflow-auto mt-7", className)}
|
<CardDescription>{description}</CardDescription>
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='pt-6'>
|
)}
|
||||||
<Table>
|
<CardContent className='pt-6'>
|
||||||
{typeof caption !== "undefined" && <TableCaption>{caption}</TableCaption>}
|
{children && (
|
||||||
|
<>
|
||||||
|
<div className='flex space-x-2'>{children}</div>
|
||||||
|
<Separator className='my-4' />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
{typeof caption !== "undefined" && <TableCaption>{caption}</TableCaption>}
|
||||||
|
{headerVisible && table.getHeaderGroups().length && (
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow key={headerGroup.id}>
|
<TableRow key={headerGroup.id} className={rowClassName}>
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => {
|
||||||
return (
|
return (
|
||||||
<TableHead
|
<TableHead
|
||||||
@ -69,35 +95,42 @@ export function DataTable<TData>({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
)}
|
||||||
{table.getRowModel().rows?.length ? (
|
<TableBody>
|
||||||
table.getRowModel().rows.map((row) => (
|
{table.getRowModel().rows?.length ? (
|
||||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
table.getRowModel().rows.map((row) => (
|
||||||
{row.getVisibleCells().map((cell) => (
|
<TableRow
|
||||||
<TableCell key={cell.id}>
|
key={row.id}
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
data-state={row.getIsSelected() && "selected"}
|
||||||
</TableCell>
|
className={rowClassName}
|
||||||
))}
|
>
|
||||||
</TableRow>
|
{row.getVisibleCells().map((cell) => (
|
||||||
))
|
<TableCell key={cell.id} className={cellClassName}>
|
||||||
) : (
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
<TableRow>
|
</TableCell>
|
||||||
<TableCell colSpan={table.getAllColumns.length} className='h-24 text-center'>
|
))}
|
||||||
No hay datos para mostrar
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
))
|
||||||
</TableBody>
|
) : (
|
||||||
</Table>
|
<TableRow className={rowClassName}>
|
||||||
</CardContent>
|
<TableCell
|
||||||
<CardFooter>
|
className={cn("h-24 text-center", cellClassName)}
|
||||||
<DataTablePagination
|
colSpan={table.getAllColumns.length}
|
||||||
className='flex-1'
|
>
|
||||||
visible={paginationOptions?.visible}
|
No hay datos para mostrar
|
||||||
table={table}
|
</TableCell>
|
||||||
/>
|
</TableRow>
|
||||||
</CardFooter>
|
)}
|
||||||
</Card>
|
</TableBody>
|
||||||
</>
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<DataTablePagination
|
||||||
|
className='flex-1'
|
||||||
|
visible={paginationOptions?.visible}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export function DataTableColumnHeader<TData, TValue>({
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:bg-accent font-semiboldw text-muted-foreground uppercase",
|
"data-[state=open]:bg-accent font-bold text-muted-foreground uppercase text-xs tracking-wide",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -68,6 +68,7 @@ export function DataTablePagination<TData>({
|
|||||||
</div>
|
</div>
|
||||||
<div className='flex items-center space-x-2'>
|
<div className='flex items-center space-x-2'>
|
||||||
<Button
|
<Button
|
||||||
|
type='button'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='hidden w-8 h-8 p-0 lg:flex'
|
className='hidden w-8 h-8 p-0 lg:flex'
|
||||||
onClick={() => table.setPageIndex(INITIAL_PAGE_INDEX)}
|
onClick={() => table.setPageIndex(INITIAL_PAGE_INDEX)}
|
||||||
@ -77,6 +78,7 @@ export function DataTablePagination<TData>({
|
|||||||
<ChevronsLeftIcon className='w-4 h-4' />
|
<ChevronsLeftIcon className='w-4 h-4' />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
type='button'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='w-8 h-8 p-0'
|
className='w-8 h-8 p-0'
|
||||||
onClick={() => table.previousPage()}
|
onClick={() => table.previousPage()}
|
||||||
@ -86,6 +88,7 @@ export function DataTablePagination<TData>({
|
|||||||
<ChevronLeftIcon className='w-4 h-4' />
|
<ChevronLeftIcon className='w-4 h-4' />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
type='button'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='w-8 h-8 p-0'
|
className='w-8 h-8 p-0'
|
||||||
onClick={() => table.nextPage()}
|
onClick={() => table.nextPage()}
|
||||||
@ -95,6 +98,7 @@ export function DataTablePagination<TData>({
|
|||||||
<ChevronRightIcon className='w-4 h-4' />
|
<ChevronRightIcon className='w-4 h-4' />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
type='button'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='hidden w-8 h-8 p-0 lg:flex'
|
className='hidden w-8 h-8 p-0 lg:flex'
|
||||||
onClick={() => table.setPageIndex(table.getPageCount() + 1)}
|
onClick={() => table.setPageIndex(table.getPageCount() + 1)}
|
||||||
|
|||||||
@ -1,24 +1,21 @@
|
|||||||
import { Checkbox } from "@/ui";
|
import { Checkbox } from "@/ui";
|
||||||
import { DataTableColumnProps } from "./DataTable";
|
import { DataTableColumnProps } from "./DataTable";
|
||||||
|
|
||||||
export function getDataTableSelectionColumn<
|
export function getDataTableSelectionColumn<TData, TError>(): DataTableColumnProps<TData, TError> {
|
||||||
TData,
|
|
||||||
TError,
|
|
||||||
>(): DataTableColumnProps<TData, TError> {
|
|
||||||
return {
|
return {
|
||||||
id: "select",
|
id: "select",
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="select-all"
|
id='select-all'
|
||||||
checked={
|
checked={
|
||||||
table.getIsAllPageRowsSelected() ||
|
table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")
|
||||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
|
||||||
}
|
}
|
||||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||||
aria-label="Seleccionar todo"
|
aria-label='Seleccionar todo'
|
||||||
className="translate-y-[2px]"
|
className='translate-y-[2px]'
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
cell: ({ row, table }) => (
|
cell: ({ row, table }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={`select-row-${row.id}`}
|
id={`select-row-${row.id}`}
|
||||||
@ -26,8 +23,8 @@ export function getDataTableSelectionColumn<
|
|||||||
onCheckedChange={(value) => {
|
onCheckedChange={(value) => {
|
||||||
row.toggleSelected(!!value);
|
row.toggleSelected(!!value);
|
||||||
}}
|
}}
|
||||||
aria-label="Seleccionar file"
|
aria-label='Seleccionar file'
|
||||||
className="translate-y-[2px]"
|
className='translate-y-[2px]'
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
|
|||||||
@ -11,10 +11,12 @@ import { DataTableColumnOptions } from "./DataTableColumnOptions";
|
|||||||
interface DataTableToolbarProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
|
interface DataTableToolbarProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
table: Table<TData>;
|
table: Table<TData>;
|
||||||
filterFields?: DataTableFilterField<TData>[];
|
filterFields?: DataTableFilterField<TData>[];
|
||||||
|
fullWidthFilter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTableToolbar<TData>({
|
export function DataTableToolbar<TData>({
|
||||||
table,
|
table,
|
||||||
|
fullWidthFilter,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
@ -35,7 +37,7 @@ export function DataTableToolbar<TData>({
|
|||||||
placeholder={t("common.filter_placeholder")}
|
placeholder={t("common.filter_placeholder")}
|
||||||
value={globalFilter}
|
value={globalFilter}
|
||||||
onChange={(event) => setGlobalFilter(String(event.target.value))}
|
onChange={(event) => setGlobalFilter(String(event.target.value))}
|
||||||
className='w-3/12 h-8 lg:w-6/12'
|
className={cn("h-8", fullWidthFilter ? "w-full" : "w-3/12 lg:w-6/12")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isFiltered && (
|
{isFiltered && (
|
||||||
|
|||||||
@ -1,126 +1,136 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import { FormControl, FormDescription, FormMessage, Input } from "@/ui";
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
Input,
|
|
||||||
} from "@/ui";
|
|
||||||
|
|
||||||
import { createElement } from "react";
|
import { IMoney } from "@/lib/types";
|
||||||
|
import { MoneyValue } from "@shared/contexts";
|
||||||
|
import { createElement, forwardRef, useState } from "react";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import { FormLabel } from "./FormLabel";
|
||||||
import { FormTextFieldProps } from "./FormTextField";
|
import { FormTextFieldProps } from "./FormTextField";
|
||||||
|
|
||||||
type FormMoneyFieldProps = Omit<FormTextFieldProps, "type">;
|
type FormMoneyFieldProps = Omit<FormTextFieldProps, "type"> & {
|
||||||
|
defaultValue?: any;
|
||||||
|
};
|
||||||
|
|
||||||
// Spanish currency config
|
export const FormMoneyField = forwardRef<
|
||||||
const moneyFormatter = Intl.NumberFormat("es-ES", {
|
HTMLDivElement,
|
||||||
currency: "EUR",
|
React.HTMLAttributes<HTMLDivElement> & FormMoneyFieldProps
|
||||||
currencyDisplay: "symbol",
|
>((props, ref) => {
|
||||||
currencySign: "standard",
|
const {
|
||||||
style: "currency",
|
label,
|
||||||
minimumFractionDigits: 2,
|
placeholder,
|
||||||
maximumFractionDigits: 2,
|
hint,
|
||||||
});
|
description,
|
||||||
|
required,
|
||||||
export function FormMoneyField({
|
className,
|
||||||
label,
|
leadIcon,
|
||||||
placeholder,
|
trailIcon,
|
||||||
hint,
|
button,
|
||||||
description,
|
disabled,
|
||||||
required,
|
name,
|
||||||
className,
|
defaultValue,
|
||||||
leadIcon,
|
} = props;
|
||||||
trailIcon,
|
|
||||||
button,
|
|
||||||
disabled,
|
|
||||||
errors,
|
|
||||||
name,
|
|
||||||
control,
|
|
||||||
}: FormMoneyFieldProps) {
|
|
||||||
/*const initialValue = props.form.getValues()[props.name]
|
|
||||||
? moneyFormatter.format(props.form.getValues()[props.name])
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const [value, setValue] = useReducer((_: any, next: string) => {
|
|
||||||
const digits = next.replace(/\D/g, "");
|
|
||||||
return moneyFormatter.format(Number(digits) / 100);
|
|
||||||
}, initialValue);
|
|
||||||
|
|
||||||
function handleChange(realChangeFn: Function, formattedValue: string) {
|
|
||||||
const digits = formattedValue.replace(/\D/g, "");
|
|
||||||
const realValue = Number(digits) / 100;
|
|
||||||
realChangeFn(realValue);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
//const error = Boolean(errors && errors[name]);
|
//const error = Boolean(errors && errors[name]);
|
||||||
|
|
||||||
|
const { control } = useFormContext();
|
||||||
|
|
||||||
|
const [precision, setPrecision] = useState<number>(MoneyValue.DEFAULT_PRECISION);
|
||||||
|
const [currencyCode, setCurrencyCode] = useState<string>(MoneyValue.DEFAULT_CURRENCY_CODE);
|
||||||
|
|
||||||
const transform = {
|
const transform = {
|
||||||
input: (value: any) =>
|
input: (value: IMoney) => {
|
||||||
isNaN(value) || value === 0 ? "" : moneyFormatter.format(value),
|
const moneyOrError = MoneyValue.create(value);
|
||||||
|
if (moneyOrError.isFailure) {
|
||||||
|
throw moneyOrError.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moneyValue = moneyOrError.object;
|
||||||
|
|
||||||
|
setPrecision(moneyValue.getPrecision());
|
||||||
|
setCurrencyCode(moneyValue.getCurrency().code);
|
||||||
|
|
||||||
|
return moneyValue.toFormat();
|
||||||
|
},
|
||||||
output: (event: React.ChangeEvent<HTMLInputElement>) => {
|
output: (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const output = parseInt(event.target.value, 10);
|
const output = parseFloat(event.target.value);
|
||||||
return isNaN(output) ? 0 : output;
|
|
||||||
|
const moneyOrError = MoneyValue.create({
|
||||||
|
amount: output * Math.pow(10, precision),
|
||||||
|
precision,
|
||||||
|
currencyCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (moneyOrError.isFailure) {
|
||||||
|
throw moneyOrError.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return moneyOrError.object.toObject();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField
|
<Controller
|
||||||
|
defaultValue={defaultValue}
|
||||||
control={control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
rules={{ required }}
|
rules={{ required }}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
render={({ field, fieldState, formState }) => {
|
render={({ field, fieldState, formState }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem className={cn(className, "space-y-3")}>
|
<input
|
||||||
|
{...field}
|
||||||
|
placeholder='number'
|
||||||
|
onChange={(e) => field.onChange(transform.output(e))}
|
||||||
|
value={transform.input(field.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(className, "space-y-3")}>
|
||||||
{label && <FormLabel label={label} hint={hint} />}
|
{label && <FormLabel label={label} hint={hint} />}
|
||||||
<div className={cn(button ? "flex" : null)}>
|
<div className={cn(button ? "flex" : null)}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
leadIcon
|
leadIcon ? "relative flex items-stretch flex-grow focus-within:z-10" : ""
|
||||||
? "relative flex items-stretch flex-grow focus-within:z-10"
|
|
||||||
: "",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{leadIcon && (
|
{leadIcon && (
|
||||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
|
||||||
{createElement(
|
{createElement(
|
||||||
leadIcon,
|
leadIcon,
|
||||||
{
|
{
|
||||||
className: "h-5 w-5 text-muted-foreground",
|
className: "h-5 w-5 text-muted-foreground",
|
||||||
"aria-hidden": true,
|
"aria-hidden": true,
|
||||||
},
|
},
|
||||||
null,
|
null
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FormControl
|
<FormControl
|
||||||
className={cn(
|
className={cn("block", leadIcon ? "pl-10" : "", trailIcon ? "pr-10" : "")}
|
||||||
"block",
|
|
||||||
leadIcon ? "pl-10" : "",
|
|
||||||
trailIcon ? "pr-10" : "",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
className={cn(
|
||||||
|
fieldState.error ? "border-destructive focus-visible:ring-destructive" : ""
|
||||||
|
)}
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => field.onChange(transform.output(e))}
|
onChange={(e) => field.onChange(transform.output(e))}
|
||||||
value={transform.input(field.value)}
|
value={transform.input(field.value)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{trailIcon && (
|
{trailIcon && (
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pl-3 pointer-events-none">
|
<div className='absolute inset-y-0 right-0 flex items-center pl-3 pointer-events-none'>
|
||||||
{createElement(
|
{createElement(
|
||||||
trailIcon,
|
trailIcon,
|
||||||
{
|
{
|
||||||
className: "h-5 w-5 text-muted-foreground",
|
className: "h-5 w-5 text-muted-foreground",
|
||||||
"aria-hidden": true,
|
"aria-hidden": true,
|
||||||
},
|
},
|
||||||
null,
|
null
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -129,9 +139,9 @@ export function FormMoneyField({
|
|||||||
</div>
|
</div>
|
||||||
{description && <FormDescription>{description}</FormDescription>}
|
{description && <FormDescription>{description}</FormDescription>}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
164
client/src/components/Forms/FormPercentageField.tsx
Normal file
164
client/src/components/Forms/FormPercentageField.tsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { FormControl, FormDescription, FormItem, InputProps } from "@/ui";
|
||||||
|
|
||||||
|
import { Percentage, PercentageObject } from "@shared/contexts";
|
||||||
|
import { createElement, forwardRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
UseControllerProps,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
|
import { FormErrorMessage } from "./FormErrorMessage";
|
||||||
|
import { FormLabel, FormLabelProps } from "./FormLabel";
|
||||||
|
import { FormInputProps, FormInputWithIconProps } from "./FormProps";
|
||||||
|
|
||||||
|
export type FormPercentageFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
|
> = {
|
||||||
|
button?: (props?: React.PropsWithChildren) => React.ReactNode;
|
||||||
|
defaultValue?: any;
|
||||||
|
} & InputProps &
|
||||||
|
FormInputProps &
|
||||||
|
Partial<FormLabelProps> &
|
||||||
|
FormInputWithIconProps &
|
||||||
|
UseControllerProps<TFieldValues, TName>;
|
||||||
|
|
||||||
|
export const FormPercentageField = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & FormPercentageFieldProps
|
||||||
|
>((props, ref) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
hint,
|
||||||
|
placeholder,
|
||||||
|
description,
|
||||||
|
|
||||||
|
required,
|
||||||
|
className,
|
||||||
|
leadIcon,
|
||||||
|
trailIcon,
|
||||||
|
button,
|
||||||
|
defaultValue,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { control } = useFormContext();
|
||||||
|
|
||||||
|
const [precision, setPrecision] = useState<number>(Percentage.DEFAULT_PRECISION);
|
||||||
|
|
||||||
|
const transform = {
|
||||||
|
input: (value: PercentageObject) => {
|
||||||
|
const percentageOrError = Percentage.create(value);
|
||||||
|
|
||||||
|
if (percentageOrError.isFailure) {
|
||||||
|
throw percentageOrError.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const percentageValue = percentageOrError.object;
|
||||||
|
setPrecision(percentageValue.getPrecision());
|
||||||
|
return percentageValue.toString();
|
||||||
|
},
|
||||||
|
output: (event: React.ChangeEvent<HTMLInputElement>): PercentageObject => {
|
||||||
|
const value = parseFloat(event.target.value);
|
||||||
|
const output = !isNaN(value) ? value : 0;
|
||||||
|
|
||||||
|
const percentageOrError = Percentage.create({
|
||||||
|
amount: output * Math.pow(10, precision),
|
||||||
|
precision,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (percentageOrError.isFailure) {
|
||||||
|
throw percentageOrError.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return percentageOrError.object.toObject();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
rules={{
|
||||||
|
required,
|
||||||
|
min: Percentage.MIN_VALUE,
|
||||||
|
max: Percentage.MAX_VALUE,
|
||||||
|
}}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
render={({ field, fieldState, formState }) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
{...field}
|
||||||
|
className='text-right'
|
||||||
|
placeholder='number'
|
||||||
|
onChange={(e) => field.onChange(transform.output(e))}
|
||||||
|
value={transform.input(field.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem ref={ref} className={cn(className, "space-y-3")}>
|
||||||
|
{label && <FormLabel label={label} hint={hint} required={required} />}
|
||||||
|
<div className={cn(button ? "flex" : null)}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
leadIcon ? "relative flex items-stretch flex-grow focus-within:z-10" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{leadIcon && (
|
||||||
|
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
|
||||||
|
{createElement(
|
||||||
|
leadIcon,
|
||||||
|
{
|
||||||
|
className: "h-5 w-5 text-muted-foreground",
|
||||||
|
"aria-hidden": true,
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormControl
|
||||||
|
className={cn("block", leadIcon ? "pl-10" : "", trailIcon ? "pr-10" : "")}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={cn(
|
||||||
|
fieldState.error ? "border-destructive focus-visible:ring-destructive" : ""
|
||||||
|
)}
|
||||||
|
{...field}
|
||||||
|
onInput={(e) => field.onChange(transform.output(e))}
|
||||||
|
value={transform.input(field.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{trailIcon && (
|
||||||
|
<div className='absolute inset-y-0 right-0 flex items-center pl-3 pointer-events-none'>
|
||||||
|
{createElement(
|
||||||
|
trailIcon,
|
||||||
|
{
|
||||||
|
className: "h-5 w-5 text-muted-foreground",
|
||||||
|
"aria-hidden": true,
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{button && <>{createElement(button)}</>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{description && <FormDescription>{description}</FormDescription>}
|
||||||
|
<FormErrorMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
162
client/src/components/Forms/FormQuantityField.tsx
Normal file
162
client/src/components/Forms/FormQuantityField.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { FormControl, FormDescription, FormItem, InputProps } from "@/ui";
|
||||||
|
|
||||||
|
import { Quantity, QuantityObject } from "@shared/contexts";
|
||||||
|
import { createElement, forwardRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
FieldPath,
|
||||||
|
FieldValues,
|
||||||
|
UseControllerProps,
|
||||||
|
useFormContext,
|
||||||
|
} from "react-hook-form";
|
||||||
|
import { FormErrorMessage } from "./FormErrorMessage";
|
||||||
|
import { FormLabel, FormLabelProps } from "./FormLabel";
|
||||||
|
import { FormInputProps, FormInputWithIconProps } from "./FormProps";
|
||||||
|
|
||||||
|
export type FormQuantityFieldProps<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
|
> = {
|
||||||
|
button?: (props?: React.PropsWithChildren) => React.ReactNode;
|
||||||
|
defaultValue?: any;
|
||||||
|
} & InputProps &
|
||||||
|
FormInputProps &
|
||||||
|
Partial<FormLabelProps> &
|
||||||
|
FormInputWithIconProps &
|
||||||
|
UseControllerProps<TFieldValues, TName>;
|
||||||
|
|
||||||
|
export const FormQuantityField = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement> & FormQuantityFieldProps
|
||||||
|
>((props, ref) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
hint,
|
||||||
|
placeholder,
|
||||||
|
description,
|
||||||
|
|
||||||
|
required,
|
||||||
|
className,
|
||||||
|
leadIcon,
|
||||||
|
trailIcon,
|
||||||
|
button,
|
||||||
|
defaultValue,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { control } = useFormContext();
|
||||||
|
|
||||||
|
const [precision, setPrecision] = useState<number>(Quantity.DEFAULT_PRECISION);
|
||||||
|
|
||||||
|
const transform = {
|
||||||
|
input: (value: QuantityObject) => {
|
||||||
|
const quantityOrError = Quantity.create(value);
|
||||||
|
|
||||||
|
if (quantityOrError.isFailure) {
|
||||||
|
throw quantityOrError.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantityValue = quantityOrError.object;
|
||||||
|
setPrecision(quantityValue.getPrecision());
|
||||||
|
return quantityValue.toString();
|
||||||
|
},
|
||||||
|
output: (event: React.ChangeEvent<HTMLInputElement>): QuantityObject => {
|
||||||
|
const value = parseFloat(event.target.value);
|
||||||
|
const output = !isNaN(value) ? value : 0;
|
||||||
|
|
||||||
|
const quantityOrError = Quantity.create({
|
||||||
|
amount: output * Math.pow(10, precision),
|
||||||
|
precision,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (quantityOrError.isFailure) {
|
||||||
|
throw quantityOrError.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return quantityOrError.object.toObject();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
control={control}
|
||||||
|
name={name}
|
||||||
|
rules={{
|
||||||
|
required,
|
||||||
|
}}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
render={({ field, fieldState, formState }) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
{...field}
|
||||||
|
className='text-right'
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(e) => field.onChange(transform.output(e))}
|
||||||
|
value={transform.input(field.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem ref={ref} className={cn(className, "space-y-3")}>
|
||||||
|
{label && <FormLabel label={label} hint={hint} required={required} />}
|
||||||
|
<div className={cn(button ? "flex" : null)}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
leadIcon ? "relative flex items-stretch flex-grow focus-within:z-10" : ""
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{leadIcon && (
|
||||||
|
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'>
|
||||||
|
{createElement(
|
||||||
|
leadIcon,
|
||||||
|
{
|
||||||
|
className: "h-5 w-5 text-muted-foreground",
|
||||||
|
"aria-hidden": true,
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormControl
|
||||||
|
className={cn("block", leadIcon ? "pl-10" : "", trailIcon ? "pr-10" : "")}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={cn(
|
||||||
|
fieldState.error ? "border-destructive focus-visible:ring-destructive" : ""
|
||||||
|
)}
|
||||||
|
{...field}
|
||||||
|
onInput={(e) => field.onChange(transform.output(e))}
|
||||||
|
value={transform.input(field.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{trailIcon && (
|
||||||
|
<div className='absolute inset-y-0 right-0 flex items-center pl-3 pointer-events-none'>
|
||||||
|
{createElement(
|
||||||
|
trailIcon,
|
||||||
|
{
|
||||||
|
className: "h-5 w-5 text-muted-foreground",
|
||||||
|
"aria-hidden": true,
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{button && <>{createElement(button)}</>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{description && <FormDescription>{description}</FormDescription>}
|
||||||
|
<FormErrorMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
@ -3,5 +3,7 @@ export * from "./FormErrorMessage";
|
|||||||
export * from "./FormGroup";
|
export * from "./FormGroup";
|
||||||
export * from "./FormLabel";
|
export * from "./FormLabel";
|
||||||
export * from "./FormMoneyField";
|
export * from "./FormMoneyField";
|
||||||
|
export * from "./FormPercentageField";
|
||||||
|
export * from "./FormQuantityField";
|
||||||
export * from "./FormTextAreaField";
|
export * from "./FormTextAreaField";
|
||||||
export * from "./FormTextField";
|
export * from "./FormTextField";
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
|
import { UnsavedChangesNotifier, UnsavedWarnProvider } from "@/lib/hooks";
|
||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren } from "react";
|
||||||
|
|
||||||
export const Layout = ({ children }: PropsWithChildren) => (
|
export const Layout = ({ children }: PropsWithChildren) => (
|
||||||
<div className='flex flex-col w-full min-h-screen'>{children}</div>
|
<UnsavedWarnProvider>
|
||||||
|
<div className='flex flex-col w-full min-h-screen'>{children}</div>
|
||||||
|
<UnsavedChangesNotifier />
|
||||||
|
</UnsavedWarnProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
Layout.displayName = "Layout";
|
Layout.displayName = "Layout";
|
||||||
|
|||||||
@ -10,7 +10,6 @@ export type LoadingIndicatorProps = {
|
|||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
export const LoadingIndicator = ({
|
export const LoadingIndicator = ({
|
||||||
active = true,
|
active = true,
|
||||||
look = "dark",
|
look = "dark",
|
||||||
|
|||||||
@ -10,6 +10,13 @@ type ProctectRouteProps = {
|
|||||||
export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
|
export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
|
||||||
const { isPending, isSuccess, data: { authenticated, redirectTo } = {} } = useIsLoggedIn();
|
const { isPending, isSuccess, data: { authenticated, redirectTo } = {} } = useIsLoggedIn();
|
||||||
|
|
||||||
|
console.debug("ProtectedRouter", {
|
||||||
|
isPending,
|
||||||
|
isSuccess,
|
||||||
|
authenticated,
|
||||||
|
redirectTo,
|
||||||
|
});
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return <LoadingOverlay />;
|
return <LoadingOverlay />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,207 +0,0 @@
|
|||||||
// https://dev.to/mperon/axios-error-handling-like-a-boss-333d
|
|
||||||
|
|
||||||
import axios, { AxiosError } from "axios";
|
|
||||||
|
|
||||||
export interface ValidationErrors {
|
|
||||||
[field: string]:
|
|
||||||
| string
|
|
||||||
| string[]
|
|
||||||
| boolean
|
|
||||||
| { key: string; message: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IHttpError extends Record<string, any> {
|
|
||||||
message: string;
|
|
||||||
statusCode: number;
|
|
||||||
errors?: ValidationErrors;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HttpError extends Error {
|
|
||||||
constructor(message?: string) {
|
|
||||||
super(message); // 'Error' breaks prototype chain here
|
|
||||||
this.name = "HttpError";
|
|
||||||
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const httpErrorHandler = (error: any) => {
|
|
||||||
if (error === null) throw new Error("Unrecoverable error!! Error is null!");
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
//here we have a type guard check, error inside this if will be treated as AxiosError
|
|
||||||
const response = error?.response;
|
|
||||||
const request = error?.request;
|
|
||||||
const config = error?.config; //here we have access the config used to make the api call (we can make a retry using this conf)
|
|
||||||
|
|
||||||
if (error.code === "ERR_NETWORK") {
|
|
||||||
console.log("connection problems..");
|
|
||||||
} else if (error.code === "ERR_CANCELED") {
|
|
||||||
console.log("connection canceled..");
|
|
||||||
}
|
|
||||||
if (response) {
|
|
||||||
//The request was made and the server responded with a status code that falls out of the range of 2xx the http status code mentioned above
|
|
||||||
const statusCode = response?.status;
|
|
||||||
if (statusCode === 404) {
|
|
||||||
console.log(
|
|
||||||
"The requested resource does not exist or has been deleted",
|
|
||||||
);
|
|
||||||
} else if (statusCode === 401) {
|
|
||||||
console.log("Please login to access this resource");
|
|
||||||
//redirect user to login
|
|
||||||
}
|
|
||||||
} else if (request) {
|
|
||||||
//The request was made but no response was received, `error.request` is an instance of XMLHttpRequest in the browser and an instance of http.ClientRequest in Node.js
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//Something happened in setting up the request and triggered an Error
|
|
||||||
console.log(error.message);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface HttpData {
|
|
||||||
code: string;
|
|
||||||
description?: string;
|
|
||||||
status: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is all errrors allowed to receive
|
|
||||||
type THttpError = Error | AxiosError | null;
|
|
||||||
|
|
||||||
// object that can be passed to our registy
|
|
||||||
interface ErrorHandlerObject {
|
|
||||||
after?(error?: THttpError, options?: ErrorHandlerObject): void;
|
|
||||||
before?(error?: THttpError, options?: ErrorHandlerObject): void;
|
|
||||||
message?: string;
|
|
||||||
notify?: any; //QNotifyOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
//signature of error function that can be passed to ours registry
|
|
||||||
type ErrorHandlerFunction = (
|
|
||||||
error?: THttpError,
|
|
||||||
) => ErrorHandlerObject | boolean | undefined;
|
|
||||||
|
|
||||||
//type that our registry accepts
|
|
||||||
type ErrorHandler = ErrorHandlerFunction | ErrorHandlerObject | string;
|
|
||||||
|
|
||||||
//interface for register many handlers once (object where key will be presented as search key for error handling
|
|
||||||
interface ErrorHandlerMany {
|
|
||||||
[key: string]: ErrorHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
// type guard to identify that is an ErrorHandlerObject
|
|
||||||
function isErrorHandlerObject(value: any): value is ErrorHandlerObject {
|
|
||||||
if (typeof value === "object") {
|
|
||||||
return ["message", "after", "before", "notify"].some((k) => k in value);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ErrorHandlerRegistry {
|
|
||||||
private handlers = new Map<string, ErrorHandler>();
|
|
||||||
|
|
||||||
private parent: ErrorHandlerRegistry | null = null;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
parent: ErrorHandlerRegistry = undefined,
|
|
||||||
input?: ErrorHandlerMany,
|
|
||||||
) {
|
|
||||||
if (typeof parent !== "undefined") this.parent = parent;
|
|
||||||
if (typeof input !== "undefined") this.registerMany(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
// allow to register an handler
|
|
||||||
register(key: string, handler: ErrorHandler) {
|
|
||||||
this.handlers.set(key, handler);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// unregister a handler
|
|
||||||
unregister(key: string) {
|
|
||||||
this.handlers.delete(key);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// search a valid handler by key
|
|
||||||
find(seek: string): ErrorHandler | undefined {
|
|
||||||
const handler = this.handlers.get(seek);
|
|
||||||
if (handler) return handler;
|
|
||||||
return this.parent?.find(seek);
|
|
||||||
}
|
|
||||||
|
|
||||||
// pass an object and register all keys/value pairs as handler.
|
|
||||||
registerMany(input: ErrorHandlerMany) {
|
|
||||||
for (const [key, value] of Object.entries(input)) {
|
|
||||||
this.register(key, value);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle error seeking for key
|
|
||||||
handleError(
|
|
||||||
this: ErrorHandlerRegistry,
|
|
||||||
seek: (string | undefined)[] | string,
|
|
||||||
error: THttpError,
|
|
||||||
): boolean {
|
|
||||||
if (Array.isArray(seek)) {
|
|
||||||
return seek.some((key) => {
|
|
||||||
if (key !== undefined) return this.handleError(String(key), error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const handler = this.find(String(seek));
|
|
||||||
if (!handler) {
|
|
||||||
return false;
|
|
||||||
} else if (typeof handler === "string") {
|
|
||||||
return this.handleErrorObject(error, { message: handler });
|
|
||||||
} else if (typeof handler === "function") {
|
|
||||||
const result = handler(error);
|
|
||||||
if (isErrorHandlerObject(result))
|
|
||||||
return this.handleErrorObject(error, result);
|
|
||||||
return !!result;
|
|
||||||
} else if (isErrorHandlerObject(handler)) {
|
|
||||||
return this.handleErrorObject(error, handler);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the error is an ErrorHandlerObject, handle here
|
|
||||||
handleErrorObject(error: THttpError, options: ErrorHandlerObject = {}) {
|
|
||||||
options?.before?.(error, options);
|
|
||||||
showToastError(options.message ?? "Unknown Error!!", options, "error");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is the function that will be registered in interceptor.
|
|
||||||
resposeErrorHandler(
|
|
||||||
this: ErrorHandlerRegistry,
|
|
||||||
error: THttpError,
|
|
||||||
direct?: boolean,
|
|
||||||
) {
|
|
||||||
if (error === null)
|
|
||||||
throw new Error("Unrecoverrable error!! Error is null!");
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
const response = error?.response;
|
|
||||||
const config = error?.config;
|
|
||||||
const data = response?.data as HttpData;
|
|
||||||
if (!direct && config?.raw) throw error;
|
|
||||||
const seekers = [
|
|
||||||
data?.code,
|
|
||||||
error.code,
|
|
||||||
error?.name,
|
|
||||||
String(data?.status),
|
|
||||||
String(response?.status),
|
|
||||||
];
|
|
||||||
const result = this.handleError(seekers, error);
|
|
||||||
if (!result) {
|
|
||||||
if (data?.code && data?.description) {
|
|
||||||
return this.handleErrorObject(error, {
|
|
||||||
message: data?.description,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (error instanceof Error) {
|
|
||||||
return this.handleError(error.name, error);
|
|
||||||
}
|
|
||||||
//if nothings works, throw away
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// create ours globalHandlers object
|
|
||||||
const globalHandlers = new ErrorHandlerRegistry();
|
|
||||||
@ -8,7 +8,6 @@ export const createAxiosAuthActions = (
|
|||||||
httpClient = createAxiosInstance()
|
httpClient = createAxiosInstance()
|
||||||
): IAuthActions => ({
|
): IAuthActions => ({
|
||||||
login: async ({ email, password }: ILogin_DTO) => {
|
login: async ({ email, password }: ILogin_DTO) => {
|
||||||
// eslint-disable-next-line no-useless-catch
|
|
||||||
try {
|
try {
|
||||||
const result = await httpClient.request<ILogin_Response_DTO>({
|
const result = await httpClient.request<ILogin_Response_DTO>({
|
||||||
url: `${apiUrl}/auth/login`,
|
url: `${apiUrl}/auth/login`,
|
||||||
@ -55,16 +54,14 @@ export const createAxiosAuthActions = (
|
|||||||
? {
|
? {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
}
|
}
|
||||||
: { authenticated: false, redirectTo: "/login" }
|
: {
|
||||||
|
authenticated: false,
|
||||||
|
redirectTo: "/login",
|
||||||
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
getIdentity: async () => {
|
getIdentity: async () => {
|
||||||
const errorResult = {
|
|
||||||
message: "Identification failed",
|
|
||||||
name: "Invalid profile or identification",
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await httpClient.request<IIdentity_Response_DTO>({
|
const result = await httpClient.request<IIdentity_Response_DTO>({
|
||||||
url: `${apiUrl}/auth/identity`,
|
url: `${apiUrl}/auth/identity`,
|
||||||
@ -78,14 +75,13 @@ export const createAxiosAuthActions = (
|
|||||||
secureLocalStorage.setItem("uecko.profile", data);
|
secureLocalStorage.setItem("uecko.profile", data);
|
||||||
return Promise.resolve(data);
|
return Promise.resolve(data);
|
||||||
}
|
}
|
||||||
return Promise.reject(errorResult);
|
return Promise.resolve(null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(errorResult);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
console.error(error);
|
|
||||||
secureLocalStorage.clear();
|
secureLocalStorage.clear();
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
error,
|
error,
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
import { IListResponse_DTO } from "@shared/contexts";
|
|
||||||
import {
|
|
||||||
IDataSource,
|
|
||||||
IGetListDataProviderParams,
|
|
||||||
IGetOneDataProviderParams,
|
|
||||||
IPaginationDataProviderParam,
|
|
||||||
} from "../hooks/useDataSource/DataSource";
|
|
||||||
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "../hooks/usePagination";
|
|
||||||
|
|
||||||
export const createJSONDataProvider = (jsonData: unknown[]): IDataSource => ({
|
|
||||||
name: () => "JSONDataProvider",
|
|
||||||
|
|
||||||
getList: <R>(params: IGetListDataProviderParams): Promise<IListResponse_DTO<R>> => {
|
|
||||||
const { resource, quickSearchTerm, pagination, filters, sort } = params;
|
|
||||||
const queryPagination = extractPaginationParams(pagination);
|
|
||||||
|
|
||||||
const items = jsonData.slice(
|
|
||||||
queryPagination.page * queryPagination.limit,
|
|
||||||
queryPagination.page * queryPagination.limit + queryPagination.limit
|
|
||||||
);
|
|
||||||
|
|
||||||
const totalItems = jsonData.length;
|
|
||||||
const totalPages = Math.ceil(totalItems / queryPagination.limit);
|
|
||||||
|
|
||||||
const response: IListResponse_DTO<R> = {
|
|
||||||
page: queryPagination.page,
|
|
||||||
per_page: queryPagination.limit,
|
|
||||||
total_pages: totalPages,
|
|
||||||
total_items: totalItems,
|
|
||||||
items,
|
|
||||||
};
|
|
||||||
|
|
||||||
return Promise.resolve(response);
|
|
||||||
},
|
|
||||||
|
|
||||||
getOne: async <R>(params: IGetOneDataProviderParams): Promise<R> => {
|
|
||||||
/*const { resource, id } = params;
|
|
||||||
|
|
||||||
const response = await httpClient.request<R>({
|
|
||||||
url: `${apiUrl}/${resource}/${id}`,
|
|
||||||
method: "GET",
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;*/
|
|
||||||
},
|
|
||||||
|
|
||||||
createOne: <P>(params: ICreateOneDataProviderParams<P>) => {
|
|
||||||
throw Error;
|
|
||||||
},
|
|
||||||
updateOne: <P>(params: IUpdateOneDataProviderParams<P>) => {
|
|
||||||
throw Error;
|
|
||||||
},
|
|
||||||
removeOne: (params: IRemoveOneDataProviderParams) => {
|
|
||||||
throw Error;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const extractPaginationParams = (pagination?: IPaginationDataProviderParam) => {
|
|
||||||
const { pageIndex = INITIAL_PAGE_INDEX, pageSize = INITIAL_PAGE_SIZE } = pagination || {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
page: pageIndex,
|
|
||||||
limit: pageSize,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./HttpError";
|
|
||||||
export * from "./createAxiosAuthActions";
|
export * from "./createAxiosAuthActions";
|
||||||
export * from "./createAxiosDataProvider";
|
export * from "./createAxiosDataProvider";
|
||||||
|
|||||||
@ -59,7 +59,7 @@ const onResponseError = (error: AxiosError): Promise<AxiosError> => {
|
|||||||
break;
|
break;
|
||||||
case 401:
|
case 401:
|
||||||
console.error("UnAuthorized");
|
console.error("UnAuthorized");
|
||||||
//return (window.location.href = "/logout");
|
return (window.location.href = "/logout");
|
||||||
break;
|
break;
|
||||||
case 403:
|
case 403:
|
||||||
console.error("Forbidden");
|
console.error("Forbidden");
|
||||||
|
|||||||
59
client/src/lib/calc.ts
Normal file
59
client/src/lib/calc.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { MoneyValue, Percentage, Quantity } from "@shared/contexts";
|
||||||
|
import { IMoney, IPercentage, IQuantity } from "./types";
|
||||||
|
|
||||||
|
export const calculateItemTotals = (item: {
|
||||||
|
quantity?: IQuantity;
|
||||||
|
retail_price?: IMoney;
|
||||||
|
discount?: IPercentage;
|
||||||
|
}): {
|
||||||
|
quantity: Quantity;
|
||||||
|
retailPrice: MoneyValue;
|
||||||
|
price: MoneyValue;
|
||||||
|
discount: Percentage;
|
||||||
|
total: MoneyValue;
|
||||||
|
} => {
|
||||||
|
const { quantity: quantityValue, retail_price: retailPriceValue, discount: discountValue } = item;
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
quantityValue,
|
||||||
|
retailPriceValue,
|
||||||
|
discountValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
const quantityOrError = Quantity.create(quantityValue);
|
||||||
|
if (quantityOrError.isFailure) {
|
||||||
|
throw quantityOrError.error;
|
||||||
|
}
|
||||||
|
const quantity = quantityOrError.object;
|
||||||
|
|
||||||
|
const retailPriceOrError = MoneyValue.create(retailPriceValue);
|
||||||
|
if (retailPriceOrError.isFailure) {
|
||||||
|
throw retailPriceOrError.error;
|
||||||
|
}
|
||||||
|
const retailPrice = retailPriceOrError.object;
|
||||||
|
|
||||||
|
const discountOrError = Percentage.create(discountValue);
|
||||||
|
if (discountOrError.isFailure) {
|
||||||
|
throw discountOrError.error;
|
||||||
|
}
|
||||||
|
const discount = discountOrError.object;
|
||||||
|
|
||||||
|
const price = retailPrice.multiply(quantity.toNumber());
|
||||||
|
const total = price.subtract(price.percentage(discount.toNumber()));
|
||||||
|
|
||||||
|
return {
|
||||||
|
quantity,
|
||||||
|
retailPrice,
|
||||||
|
price,
|
||||||
|
discount,
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*return {
|
||||||
|
quantity: quantity.toObject(),
|
||||||
|
retail_price: retailPrice.toObject(),
|
||||||
|
price: price.toObject(),
|
||||||
|
discount: discount.toObject(),
|
||||||
|
total: total.toObject(),
|
||||||
|
};*/
|
||||||
|
};
|
||||||
@ -13,7 +13,9 @@ export * from "./useUrlId";
|
|||||||
|
|
||||||
export * from "./useAuth";
|
export * from "./useAuth";
|
||||||
export * from "./useCustomDialog";
|
export * from "./useCustomDialog";
|
||||||
|
export * from "./useDataSource";
|
||||||
export * from "./useDataTable";
|
export * from "./useDataTable";
|
||||||
export * from "./useLocalization";
|
export * from "./useLocalization";
|
||||||
export * from "./usePagination";
|
export * from "./usePagination";
|
||||||
export * from "./useTheme";
|
export * from "./useTheme";
|
||||||
|
export * from "./useUnsavedChangesNotifier";
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export type SuccessNotificationResponse = {
|
|||||||
|
|
||||||
export type PermissionResponse = unknown;
|
export type PermissionResponse = unknown;
|
||||||
|
|
||||||
export type IdentityResponse = IIdentity_Response_DTO;
|
export type IdentityResponse = IIdentity_Response_DTO | null;
|
||||||
|
|
||||||
export type AuthActionCheckResponse = {
|
export type AuthActionCheckResponse = {
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export const AuthProvider = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCheck = async () => {
|
const handleCheck = async () => {
|
||||||
|
console.trace("check");
|
||||||
try {
|
try {
|
||||||
return Promise.resolve(authActions.check?.());
|
return Promise.resolve(authActions.check?.());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export const useIsLoggedIn = (queryOptions?: UseQueryOptions<AuthActionCheckResp
|
|||||||
const result = useQuery<AuthActionCheckResponse>({
|
const result = useQuery<AuthActionCheckResponse>({
|
||||||
queryKey: keys().auth().action("check").get(),
|
queryKey: keys().auth().action("check").get(),
|
||||||
queryFn: check,
|
queryFn: check,
|
||||||
|
retry: false,
|
||||||
...queryOptions,
|
...queryOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useMatches } from "react-router";
|
|
||||||
|
|
||||||
export const useBreadcrumbs = (): any[] => {
|
|
||||||
const [crumbs, setCrumbs] = useState<any[]>([]);
|
|
||||||
let matches = useMatches();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const _crumbs = matches
|
|
||||||
// @ts-ignore
|
|
||||||
.filter((match) => Boolean(match.handle?.crumb))
|
|
||||||
// @ts-ignore
|
|
||||||
.map((match) => match.handle?.crumb(match.data));
|
|
||||||
|
|
||||||
setCrumbs(_crumbs);
|
|
||||||
}, matches);
|
|
||||||
|
|
||||||
return crumbs;
|
|
||||||
};
|
|
||||||
@ -1,37 +1,30 @@
|
|||||||
import {
|
import { QueryFunction, QueryKey, UseQueryResult, useQuery } from "@tanstack/react-query";
|
||||||
QueryFunction,
|
|
||||||
QueryKey,
|
|
||||||
UseQueryResult,
|
|
||||||
useQuery,
|
|
||||||
} from '@tanstack/react-query';
|
|
||||||
|
|
||||||
|
|
||||||
export interface IUseManyQueryOptions<
|
export interface IUseManyQueryOptions<
|
||||||
TUseManyQueryData = unknown,
|
TUseManyQueryData = unknown,
|
||||||
TUseManyQueryError = unknown
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
TUseManyQueryError = unknown
|
||||||
> {
|
> {
|
||||||
queryKey: QueryKey;
|
queryKey: QueryKey;
|
||||||
queryFn: QueryFunction<TUseManyQueryData, QueryKey>;
|
queryFn: QueryFunction<TUseManyQueryData, QueryKey>;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
select?: (data: TUseManyQueryData) => TUseManyQueryData;
|
select?: (data: TUseManyQueryData) => TUseManyQueryData;
|
||||||
queryOptions?: any;
|
queryOptions?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMany<TUseManyQueryData, TUseManyQueryError>(
|
export function useMany<TUseManyQueryData, TUseManyQueryError>(
|
||||||
options: IUseManyQueryOptions<TUseManyQueryData, TUseManyQueryError>
|
options: IUseManyQueryOptions<TUseManyQueryData, TUseManyQueryError>
|
||||||
): UseQueryResult<TUseManyQueryData, TUseManyQueryError> {
|
): UseQueryResult<TUseManyQueryData, TUseManyQueryError> {
|
||||||
const { queryKey, queryFn, enabled, select, queryOptions } = options;
|
const { queryKey, queryFn, enabled, select, queryOptions } = options;
|
||||||
|
|
||||||
const queryResponse = useQuery<TUseManyQueryData, TUseManyQueryError>({
|
|
||||||
queryKey,
|
|
||||||
queryFn,
|
|
||||||
keepPreviousData: true,
|
|
||||||
...queryOptions,
|
|
||||||
enabled,
|
|
||||||
select,
|
|
||||||
|
|
||||||
});
|
const queryResponse = useQuery<TUseManyQueryData, TUseManyQueryError>({
|
||||||
|
queryKey,
|
||||||
|
queryFn,
|
||||||
|
keepPreviousData: true,
|
||||||
|
...queryOptions,
|
||||||
|
enabled,
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
|
||||||
return queryResponse;
|
return queryResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,30 +1,29 @@
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
export interface IUseRemoveMutationOptions<
|
export interface IUseRemoveMutationOptions<
|
||||||
TUseRemoveMutationData,
|
TUseRemoveMutationData,
|
||||||
TUseRemoveMutationError,
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
TUseRemoveMutationVariables
|
TUseRemoveMutationError,
|
||||||
|
TUseRemoveMutationVariables
|
||||||
> {
|
> {
|
||||||
mutationFn: (
|
mutationFn: (variables: TUseRemoveMutationVariables) => Promise<TUseRemoveMutationData>;
|
||||||
variables: TUseRemoveMutationVariables,
|
|
||||||
) => Promise<TUseRemoveMutationData>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRemove<
|
export function useRemove<
|
||||||
|
TUseRemoveMutationData,
|
||||||
|
TUseRemoveMutationError,
|
||||||
|
TUseRemoveMutationVariables
|
||||||
|
>(
|
||||||
|
options: IUseRemoveMutationOptions<
|
||||||
TUseRemoveMutationData,
|
TUseRemoveMutationData,
|
||||||
TUseRemoveMutationError,
|
TUseRemoveMutationError,
|
||||||
TUseRemoveMutationVariables>(options: IUseRemoveMutationOptions<
|
TUseRemoveMutationVariables
|
||||||
TUseRemoveMutationData,
|
>
|
||||||
TUseRemoveMutationError,
|
) {
|
||||||
TUseRemoveMutationVariables
|
const { mutationFn, ...params } = options;
|
||||||
>) {
|
|
||||||
const { mutationFn, ...params } = options;
|
|
||||||
|
|
||||||
return useMutation<
|
return useMutation<TUseRemoveMutationData, TUseRemoveMutationError, TUseRemoveMutationVariables>({
|
||||||
TUseRemoveMutationData,
|
mutationFn,
|
||||||
TUseRemoveMutationError,
|
...params,
|
||||||
TUseRemoveMutationVariables>({
|
});
|
||||||
mutationFn,
|
|
||||||
...params
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,21 @@
|
|||||||
import { PaginationState, usePaginationParams } from "@/lib/hooks";
|
import { PaginationState } from "@/lib/hooks";
|
||||||
import { SortingState } from "@tanstack/react-table";
|
import { SortingState } from "@tanstack/react-table";
|
||||||
import { PropsWithChildren, createContext, useCallback, useMemo, useState } from "react";
|
import {
|
||||||
|
Dispatch,
|
||||||
|
PropsWithChildren,
|
||||||
|
SetStateAction,
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { useSyncedPagination } from "./useSyncedPagination";
|
||||||
|
|
||||||
export interface IDataTableContextState {
|
export interface IDataTableContextState {
|
||||||
pagination: PaginationState;
|
pagination: PaginationState;
|
||||||
setPagination: (newPagination: PaginationState) => void;
|
setPagination: (newPagination: PaginationState) => void;
|
||||||
sorting: [];
|
sorting: SortingState;
|
||||||
setSorting: () => void;
|
setSorting: Dispatch<SetStateAction<SortingState>>;
|
||||||
globalFilter: string;
|
globalFilter: string;
|
||||||
setGlobalFilter: (newGlobalFilter: string) => void;
|
setGlobalFilter: (newGlobalFilter: string) => void;
|
||||||
resetGlobalFilter: () => void;
|
resetGlobalFilter: () => void;
|
||||||
@ -16,12 +25,14 @@ export interface IDataTableContextState {
|
|||||||
export const DataTableContext = createContext<IDataTableContextState | null>(null);
|
export const DataTableContext = createContext<IDataTableContextState | null>(null);
|
||||||
|
|
||||||
export const DataTableProvider = ({
|
export const DataTableProvider = ({
|
||||||
|
syncWithLocation = true,
|
||||||
initialGlobalFilter = "",
|
initialGlobalFilter = "",
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<{
|
}: PropsWithChildren<{
|
||||||
|
syncWithLocation?: boolean;
|
||||||
initialGlobalFilter?: string;
|
initialGlobalFilter?: string;
|
||||||
}>) => {
|
}>) => {
|
||||||
const [pagination, setPagination] = usePaginationParams();
|
const [pagination, setPagination] = useSyncedPagination(syncWithLocation);
|
||||||
const [globalFilter, setGlobalFilter] = useState<string>(initialGlobalFilter);
|
const [globalFilter, setGlobalFilter] = useState<string>(initialGlobalFilter);
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
|
|||||||
@ -1,192 +0,0 @@
|
|||||||
import { differenceWith, isEmpty, isEqual, reverse, unionWith } from 'lodash';
|
|
||||||
|
|
||||||
export const parseQueryString = (queryString: Record<string, string>) => {
|
|
||||||
// pagination
|
|
||||||
const pageIndex = parseInt(queryString.page ?? '0', 0) - 1;
|
|
||||||
const pageSize = Math.min(parseInt(queryString.limit ?? '10', 10), 100);
|
|
||||||
const pagination =
|
|
||||||
pageIndex >= 0 && pageSize > 0 ? { pageIndex, pageSize } : undefined;
|
|
||||||
|
|
||||||
// pagination
|
|
||||||
/*let pagination = undefined;
|
|
||||||
if (page !== undefined && limit !== undefined) {
|
|
||||||
let parsedPage = toSafeInteger(queryString['page']) - 1;
|
|
||||||
if (parsedPage < 0) parsedPage = 0;
|
|
||||||
|
|
||||||
let parsedPageSize = toSafeInteger(queryString['limit']);
|
|
||||||
if (parsedPageSize > 100) parsedPageSize = 100;
|
|
||||||
if (parsedPageSize < 5) parsedPageSize = 5;
|
|
||||||
|
|
||||||
pagination = {
|
|
||||||
pageIndex: parsedPage,
|
|
||||||
pageSize: parsedPageSize,
|
|
||||||
};
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// sorter
|
|
||||||
// sorter
|
|
||||||
const sorter = (queryString.sort ?? '')
|
|
||||||
.split(',')
|
|
||||||
.map((token) => token.match(/([+-]?)([\w_]+)/i))
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((item) => (item ? { id: item[2], desc: item[1] === '-' } : null))
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
/*let sorter = [];
|
|
||||||
if (sort !== undefined) {
|
|
||||||
sorter = sort
|
|
||||||
.split(',')
|
|
||||||
.map((token) => token.match(/([+-]?)([\w_]+)/i))
|
|
||||||
.slice(0, 3)
|
|
||||||
.map((item) =>
|
|
||||||
item ? { id: item[2], desc: item[1] === '-' } : null
|
|
||||||
);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// filters
|
|
||||||
const filters = Object.entries(queryString)
|
|
||||||
.filter(([key]) => key !== 'page' && key !== 'limit' && key !== 'sort')
|
|
||||||
.map(([key, value]) => {
|
|
||||||
const [, field, , , operator] =
|
|
||||||
key.match(/([\w]+)(([\[])([\w]+)([\]]))*/i) ?? [];
|
|
||||||
const sanitizedOperator = _sanitizeOperator(operator ?? '');
|
|
||||||
return !isEmpty(value)
|
|
||||||
? { field, operator: sanitizedOperator, value }
|
|
||||||
: null;
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
/*let filters = [];
|
|
||||||
if (filterCandidates !== undefined) {
|
|
||||||
Object.keys(filterCandidates).map((token) => {
|
|
||||||
const [, field, , , operator] = token.match( */
|
|
||||||
// /([\w]+)(([\[])([\w]+)([\]]))*/i
|
|
||||||
/* );
|
|
||||||
const value = filterCandidates[token];
|
|
||||||
|
|
||||||
if (!isEmpty(value)) {
|
|
||||||
filters.push({
|
|
||||||
field,
|
|
||||||
operator: _sanitizeOperator(operator),
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}*/
|
|
||||||
|
|
||||||
return {
|
|
||||||
pagination,
|
|
||||||
sorter,
|
|
||||||
filters,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildQueryString = ({ pagination, sorter, filters }) => {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
if (
|
|
||||||
pagination &&
|
|
||||||
pagination.pageIndex !== undefined &&
|
|
||||||
pagination.pageSize !== undefined
|
|
||||||
) {
|
|
||||||
params.append('page', String(pagination.pageIndex + 1));
|
|
||||||
params.append('limit', String(pagination.pageSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sorter && Array.isArray(sorter) && sorter.length > 0) {
|
|
||||||
params.append(
|
|
||||||
'sort',
|
|
||||||
sorter.map(({ id, desc }) => `${desc ? '-' : ''}${id}`).toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filters && Array.isArray(filters) && filters.length > 0) {
|
|
||||||
filters.forEach((filterItem) => {
|
|
||||||
if (filterItem.value !== undefined) {
|
|
||||||
let operator = _mapFilterOperator(filterItem.operator);
|
|
||||||
if (operator === 'eq') {
|
|
||||||
params.append(`${filterItem.field}`, filterItem.value);
|
|
||||||
} else {
|
|
||||||
params.append(
|
|
||||||
`${filterItem.field}[${_mapFilterOperator(
|
|
||||||
filterItem.operator
|
|
||||||
)}]`,
|
|
||||||
filterItem.value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return params.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const combineFilters = (requiredFilters = [], otherFilters = []) => [
|
|
||||||
...differenceWith(otherFilters, requiredFilters, isEqual),
|
|
||||||
...requiredFilters,
|
|
||||||
];
|
|
||||||
|
|
||||||
export const unionFilters = (
|
|
||||||
permanentFilter = [],
|
|
||||||
newFilters = [],
|
|
||||||
prevFilters = []
|
|
||||||
) =>
|
|
||||||
reverse(
|
|
||||||
unionWith(
|
|
||||||
permanentFilter,
|
|
||||||
newFilters,
|
|
||||||
prevFilters,
|
|
||||||
(left, right) =>
|
|
||||||
left.field == right.field && left.operator == right.operator
|
|
||||||
)
|
|
||||||
).filter(
|
|
||||||
(crudFilter) =>
|
|
||||||
crudFilter.value !== undefined && crudFilter.value !== null
|
|
||||||
);
|
|
||||||
|
|
||||||
export const extractTableSortPropertiesFromColumn = (columns) => {
|
|
||||||
const _extractColumnSortProperies = (column) => {
|
|
||||||
const { canSort, isSorted, sortedIndex, isSortedDesc } = column;
|
|
||||||
if (!isSorted || !canSort) {
|
|
||||||
return undefined;
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
index: sortedIndex,
|
|
||||||
field: column.id,
|
|
||||||
order: isSortedDesc ? 'DESC' : 'ASC',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return columns
|
|
||||||
.map((column) => _extractColumnSortProperies(column))
|
|
||||||
.filter((item) => item)
|
|
||||||
.sort((a, b) => a.index - b.index);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const extractTableSortProperties = (sorter) => {
|
|
||||||
return sorter.map((sortItem, index) => ({
|
|
||||||
index,
|
|
||||||
field: sortItem.id,
|
|
||||||
order: sortItem.desc ? 'DESC' : 'ASC',
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const extractTableFilterProperties = (filters) =>
|
|
||||||
filters.filter((item) => !isEmpty(item.value));
|
|
||||||
|
|
||||||
const _sanitizeOperator = (operator) =>
|
|
||||||
['eq', 'ne', 'gte', 'lte', 'like'].includes(operator) ? operator : 'eq';
|
|
||||||
|
|
||||||
const _mapFilterOperator = (operator) => {
|
|
||||||
switch (operator) {
|
|
||||||
case 'ne':
|
|
||||||
case 'gte':
|
|
||||||
case 'lte':
|
|
||||||
return `[${operator}]`;
|
|
||||||
case 'contains':
|
|
||||||
return '[like]';
|
|
||||||
default:
|
|
||||||
return 'eq';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,7 +1,4 @@
|
|||||||
import {
|
import { DataTableColumnProps, getDataTableSelectionColumn } from "@/components";
|
||||||
DataTableColumnProps,
|
|
||||||
getDataTableSelectionColumn,
|
|
||||||
} from "@/components";
|
|
||||||
import { IListResponse_DTO } from "@shared/contexts";
|
import { IListResponse_DTO } from "@shared/contexts";
|
||||||
import {
|
import {
|
||||||
OnChangeFn,
|
OnChangeFn,
|
||||||
@ -12,12 +9,9 @@ import {
|
|||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { UseListQueryResult } from "../useDataSource";
|
import { UseListQueryResult } from "../useDataSource";
|
||||||
import { usePaginationParams } from "../usePagination";
|
import { usePaginationSyncWithLocation } from "../usePagination";
|
||||||
|
|
||||||
type TUseDataTableQueryResult<TData, TError> = UseListQueryResult<
|
type TUseDataTableQueryResult<TData, TError> = UseListQueryResult<IListResponse_DTO<TData>, TError>;
|
||||||
IListResponse_DTO<TData>,
|
|
||||||
TError
|
|
||||||
>;
|
|
||||||
|
|
||||||
type TUseDataTableQuery<TData, TError> = (params: {
|
type TUseDataTableQuery<TData, TError> = (params: {
|
||||||
pagination: {
|
pagination: {
|
||||||
@ -44,16 +38,9 @@ type DataTableColumnsOptionsProps<TData, TValue> = {
|
|||||||
columns: DataTableColumnsProps<TData, TValue>;
|
columns: DataTableColumnsProps<TData, TValue>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DataTableColumnsProps<TData, TValue> = DataTableColumnProps<
|
type DataTableColumnsProps<TData, TValue> = DataTableColumnProps<TData, TValue>[];
|
||||||
TData,
|
|
||||||
TValue
|
|
||||||
>[];
|
|
||||||
|
|
||||||
export const useQueryDataTable = <
|
export const useQueryDataTable = <TData = unknown, TValue = unknown, TError = Error>({
|
||||||
TData = unknown,
|
|
||||||
TValue = unknown,
|
|
||||||
TError = Error
|
|
||||||
>({
|
|
||||||
fetchQuery,
|
fetchQuery,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
|
|
||||||
@ -64,7 +51,7 @@ export const useQueryDataTable = <
|
|||||||
const defaultData = useMemo(() => [], []);
|
const defaultData = useMemo(() => [], []);
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState({});
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
const [pagination, setPagination] = usePaginationParams();
|
const [pagination, setPagination] = usePaginationSyncWithLocation();
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
|
||||||
const paginationUpdater: OnChangeFn<PaginationState> = (updater) => {
|
const paginationUpdater: OnChangeFn<PaginationState> = (updater) => {
|
||||||
|
|||||||
12
client/src/lib/hooks/useDataTable/useSyncedPagination.tsx
Normal file
12
client/src/lib/hooks/useDataTable/useSyncedPagination.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { usePagination, usePaginationSyncWithLocation } from "../usePagination";
|
||||||
|
|
||||||
|
export const useSyncedPagination = (syncWithLocation: boolean) => {
|
||||||
|
const [paginationWithLocation, setPaginationWithLocation] = usePaginationSyncWithLocation();
|
||||||
|
const [paginationWithoutLocation, setPaginationWithoutLocation] = usePagination();
|
||||||
|
|
||||||
|
if (syncWithLocation) {
|
||||||
|
return [paginationWithLocation, setPaginationWithLocation] as const;
|
||||||
|
} else {
|
||||||
|
return [paginationWithoutLocation, setPaginationWithoutLocation] as const;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,7 +1,6 @@
|
|||||||
/* https://github.com/mayank8aug/use-localization/blob/main/src/index.ts */
|
/* https://github.com/mayank8aug/use-localization/blob/main/src/index.ts */
|
||||||
|
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { LocaleToCurrencyTable, rtlLangsList } from "./utils";
|
import { LocaleToCurrencyTable, rtlLangsList } from "./utils";
|
||||||
|
|
||||||
type UseLocalizationProps = {
|
type UseLocalizationProps = {
|
||||||
@ -12,10 +11,10 @@ export const useLocalization = (props: UseLocalizationProps) => {
|
|||||||
const { locale } = props;
|
const { locale } = props;
|
||||||
const [lang, loc] = locale.split("-");
|
const [lang, loc] = locale.split("-");
|
||||||
|
|
||||||
const { i18n } = useTranslation();
|
//const { i18n } = useTranslation();
|
||||||
|
|
||||||
// Obtener el idioma actual
|
// Obtener el idioma actual
|
||||||
const currentLanguage = i18n.language;
|
// const currentLanguage = i18n.language;
|
||||||
|
|
||||||
const formatCurrency = useCallback(
|
const formatCurrency = useCallback(
|
||||||
(value: number) => {
|
(value: number) => {
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
export * from "./usePagination";
|
export * from "./usePagination";
|
||||||
export * from "./usePaginationParams";
|
export * from "./usePaginationSyncWithLocation";
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { useMemo } from "react";
|
|||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { usePagination } from "./usePagination";
|
import { usePagination } from "./usePagination";
|
||||||
|
|
||||||
export const usePaginationParams = (
|
export const usePaginationSyncWithLocation = (
|
||||||
initialPageIndex: number = INITIAL_PAGE_INDEX,
|
initialPageIndex: number = INITIAL_PAGE_INDEX,
|
||||||
initialPageSize: number = INITIAL_PAGE_SIZE
|
initialPageSize: number = INITIAL_PAGE_SIZE
|
||||||
) => {
|
) => {
|
||||||
@ -50,5 +50,5 @@ export const usePaginationParams = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return [pagination, updatePagination];
|
return [pagination, updatePagination] as const;
|
||||||
};
|
};
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { PropsWithChildren, createContext, useState } from "react";
|
||||||
|
|
||||||
|
export interface IUnsavedWarnContextState {
|
||||||
|
warnWhen?: boolean;
|
||||||
|
setWarnWhen?: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnsavedWarnContext = createContext<IUnsavedWarnContextState>({});
|
||||||
|
|
||||||
|
export const UnsavedWarnProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const [warnWhen, setWarnWhen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnsavedWarnContext.Provider value={{ warnWhen, setWarnWhen }}>
|
||||||
|
{children}
|
||||||
|
</UnsavedWarnContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1 +1,3 @@
|
|||||||
|
export * from "./WarnAboutChangeContext";
|
||||||
export * from "./useUnsavedChangesNotifier";
|
export * from "./useUnsavedChangesNotifier";
|
||||||
|
export * from "./useWarnAboutChange";
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* `useBlocker` and `usePrompt` is no longer part of react-router-dom for the routers other than `DataRouter`.
|
||||||
|
*
|
||||||
|
* The previous workaround (<v6.4) was to use `block` function in `UNSAFE_NavigationContext` which is now removed.
|
||||||
|
*
|
||||||
|
* We're using a workaround from the gist https://gist.github.com/MarksCode/64e438c82b0b2a1161e01c88ca0d0355 with some modifications
|
||||||
|
* Thanks to @MarksCode(https://github.com/MarksCode) for the workaround.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { UNSAFE_NavigationContext as NavigationContext } from "react-router-dom";
|
||||||
|
|
||||||
|
function useConfirmExit(confirmExit: () => boolean, when = true) {
|
||||||
|
const { navigator } = React.useContext(NavigationContext);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!when) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const go = navigator.go;
|
||||||
|
const push = navigator.push;
|
||||||
|
|
||||||
|
navigator.push = (...args: Parameters<typeof push>) => {
|
||||||
|
const result = confirmExit();
|
||||||
|
if (result !== false) {
|
||||||
|
push(...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
navigator.go = (...args: Parameters<typeof go>) => {
|
||||||
|
const result = confirmExit();
|
||||||
|
if (result !== false) {
|
||||||
|
go(...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
navigator.push = push;
|
||||||
|
navigator.go = go;
|
||||||
|
};
|
||||||
|
}, [navigator, confirmExit, when]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePrompt(message: string, when = true, onConfirm?: () => void, legacy = false) {
|
||||||
|
const warnWhenListener = React.useCallback(
|
||||||
|
(e: { preventDefault: () => void; returnValue: string }) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
e.returnValue = message;
|
||||||
|
|
||||||
|
return e.returnValue;
|
||||||
|
},
|
||||||
|
[message]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (when && !legacy) {
|
||||||
|
window.addEventListener("beforeunload", warnWhenListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("beforeunload", warnWhenListener);
|
||||||
|
};
|
||||||
|
}, [warnWhenListener, when, legacy]);
|
||||||
|
|
||||||
|
const confirmExit = React.useCallback(() => {
|
||||||
|
const confirm = window.confirm(message);
|
||||||
|
if (confirm && onConfirm) {
|
||||||
|
onConfirm();
|
||||||
|
}
|
||||||
|
return confirm;
|
||||||
|
}, [message]);
|
||||||
|
|
||||||
|
console.log(when);
|
||||||
|
|
||||||
|
useConfirmExit(confirmExit, when);
|
||||||
|
}
|
||||||
@ -1,16 +1,44 @@
|
|||||||
import React from "react";
|
import { CustomDialog } from "@/components";
|
||||||
import { useCustomDialog } from "../useCustomDialog";
|
import { t } from "i18next";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { useWarnAboutChange } from "./useWarnAboutChange";
|
||||||
|
|
||||||
type UnsavedChangesNotifierProps = {
|
type UnsavedChangesNotifierProps = {
|
||||||
translationKey?: string;
|
|
||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UnsavedChangesNotifier: React.FC<UnsavedChangesNotifierProps> = () => {
|
export const UnsavedChangesNotifier = ({
|
||||||
const { openDialog: openWarmDialog, DialogComponent: WarmDialog } = useCustomDialog({
|
message = t("unsaved_changes_prompt"),
|
||||||
title: "Hay cambios sin guardar",
|
}: UnsavedChangesNotifierProps) => {
|
||||||
description: "Are you sure you want to leave? You have unsaved changes.",
|
const { pathname } = useLocation();
|
||||||
|
const { warnWhen, setWarnWhen } = useWarnAboutChange();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => setWarnWhen?.(false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
const warnMessage = useMemo(() => {
|
||||||
|
return t(message);
|
||||||
|
}, [message]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomDialog
|
||||||
|
cancelLabel='Cancelar'
|
||||||
|
confirmLabel='Confirmar'
|
||||||
|
onCancel={() => {}}
|
||||||
|
title='titulo'
|
||||||
|
isOpen={warnWhen}
|
||||||
|
onConfirm={() => {
|
||||||
|
setWarnWhen?.(false);
|
||||||
|
}}
|
||||||
|
description={warnMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
/*usePrompt(warnMessage, warnWhen, () => {
|
||||||
|
setWarnWhen?.(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
return <>{WarmDialog}</>;
|
return null;*/
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { UnsavedWarnContext } from "./WarnAboutChangeContext";
|
||||||
|
|
||||||
|
export const useWarnAboutChange = () => {
|
||||||
|
const context = useContext(UnsavedWarnContext);
|
||||||
|
if (context === null)
|
||||||
|
throw new Error("useWarnAboutChange must be used within a UnsavedWarnProvider");
|
||||||
|
|
||||||
|
const { warnWhen, setWarnWhen } = context;
|
||||||
|
|
||||||
|
return {
|
||||||
|
warnWhen: Boolean(warnWhen),
|
||||||
|
setWarnWhen: setWarnWhen ?? (() => undefined),
|
||||||
|
};
|
||||||
|
};
|
||||||
9
client/src/lib/types.ts
Normal file
9
client/src/lib/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import {
|
||||||
|
IMoney_Response_DTO,
|
||||||
|
IPercentage_Response_DTO,
|
||||||
|
IQuantity_Response_DTO,
|
||||||
|
} from "@shared/contexts";
|
||||||
|
|
||||||
|
export interface IMoney extends IMoney_Response_DTO {}
|
||||||
|
export interface IQuantity extends IQuantity_Response_DTO {}
|
||||||
|
export interface IPercentage extends IPercentage_Response_DTO {}
|
||||||
@ -30,7 +30,8 @@
|
|||||||
"duplicate_rows": "Duplicar",
|
"duplicate_rows": "Duplicar",
|
||||||
"duplicate_rows_tooltip": "Duplica las fila(s) seleccionadas(s)",
|
"duplicate_rows_tooltip": "Duplica las fila(s) seleccionadas(s)",
|
||||||
"pick_date": "Elige una fecha",
|
"pick_date": "Elige una fecha",
|
||||||
"required_field": "Este campo es obligatorio"
|
"required_field": "Este campo es obligatorio",
|
||||||
|
"unsaved_changes_prompt": "Los últimos cambios no se han guardado. Si continúas, se perderán"
|
||||||
},
|
},
|
||||||
"main_menu": {
|
"main_menu": {
|
||||||
"home": "Inicio",
|
"home": "Inicio",
|
||||||
@ -146,6 +147,9 @@
|
|||||||
"desc": "desc"
|
"desc": "desc"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"title": "Cotización"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export const useAutosizeTextArea = ({
|
|||||||
minHeight = 0,
|
minHeight = 0,
|
||||||
}: UseAutosizeTextAreaProps) => {
|
}: UseAutosizeTextAreaProps) => {
|
||||||
const [init, setInit] = React.useState(true);
|
const [init, setInit] = React.useState(true);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
|
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
|
||||||
const offsetBorder = 2;
|
const offsetBorder = 2;
|
||||||
@ -36,5 +37,5 @@ export const useAutosizeTextArea = ({
|
|||||||
textAreaRef.style.height = `${scrollHeight + offsetBorder}px`;
|
textAreaRef.style.height = `${scrollHeight + offsetBorder}px`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [textAreaRef, triggerAutoSize]);
|
}, [textAreaRef, triggerAutoSize, init, maxHeight, minHeight]);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,13 +4,14 @@ import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
|||||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||||
import {
|
import {
|
||||||
Collection,
|
Collection,
|
||||||
Currency,
|
CurrencyData,
|
||||||
Description,
|
Description,
|
||||||
DomainError,
|
DomainError,
|
||||||
ICreateQuote_Request_DTO,
|
ICreateQuote_Request_DTO,
|
||||||
IDomainError,
|
IDomainError,
|
||||||
Language,
|
Language,
|
||||||
Note,
|
Note,
|
||||||
|
Percentage,
|
||||||
Quantity,
|
Quantity,
|
||||||
Result,
|
Result,
|
||||||
UTCDateValue,
|
UTCDateValue,
|
||||||
@ -153,7 +154,7 @@ export class CreateQuoteUseCase
|
|||||||
return Result.fail(customerOrError.error);
|
return Result.fail(customerOrError.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currencyOrError = Currency.createFromCode(quoteDTO.currency_code);
|
const currencyOrError = CurrencyData.createFromCode(quoteDTO.currency_code);
|
||||||
if (currencyOrError.isFailure) {
|
if (currencyOrError.isFailure) {
|
||||||
return Result.fail(currencyOrError.error);
|
return Result.fail(currencyOrError.error);
|
||||||
}
|
}
|
||||||
@ -177,13 +178,15 @@ export class CreateQuoteUseCase
|
|||||||
quoteDTO.items?.map(
|
quoteDTO.items?.map(
|
||||||
(item) =>
|
(item) =>
|
||||||
QuoteItem.create({
|
QuoteItem.create({
|
||||||
|
articleId: item.article_id,
|
||||||
description: Description.create(item.description).object,
|
description: Description.create(item.description).object,
|
||||||
quantity: Quantity.create({ amount: item.quantity, precision: 4 }).object,
|
quantity: Quantity.create(item.quantity).object,
|
||||||
unitPrice: UnitPrice.create({
|
unitPrice: UnitPrice.create({
|
||||||
amount: item.unit_price.amount,
|
amount: item.unit_price.amount,
|
||||||
currencyCode: item.unit_price.currency,
|
currencyCode: item.unit_price.currency_code,
|
||||||
precision: item.unit_price.precision,
|
precision: item.unit_price.precision,
|
||||||
}).object,
|
}).object,
|
||||||
|
discount: Percentage.create(item.discount.amount).object,
|
||||||
}).object
|
}).object
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,10 +7,11 @@ import {
|
|||||||
import { IRepositoryManager } from "@/contexts/common/domain";
|
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||||
import { Result, UniqueID } from "@shared/contexts";
|
import { Result, UniqueID } from "@shared/contexts";
|
||||||
import { IQuoteRepository } from "../../domain";
|
import { Dealer, IQuoteRepository } from "../../domain";
|
||||||
|
|
||||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||||
import { Quote } from "../../domain/entities/Quotes/Quote";
|
import { Quote } from "../../domain/entities/Quotes/Quote";
|
||||||
|
import { ISalesContext } from "../../infrastructure";
|
||||||
|
|
||||||
export interface IGetQuoteUseCaseRequest extends IUseCaseRequest {
|
export interface IGetQuoteUseCaseRequest extends IUseCaseRequest {
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
@ -25,14 +26,12 @@ export class GetQuoteUseCase
|
|||||||
{
|
{
|
||||||
private _adapter: ISequelizeAdapter;
|
private _adapter: ISequelizeAdapter;
|
||||||
private _repositoryManager: IRepositoryManager;
|
private _repositoryManager: IRepositoryManager;
|
||||||
|
private _dealer?: Dealer;
|
||||||
|
|
||||||
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
|
constructor(context: ISalesContext) {
|
||||||
this._adapter = props.adapter;
|
this._adapter = context.adapter;
|
||||||
this._repositoryManager = props.repositoryManager;
|
this._repositoryManager = context.repositoryManager;
|
||||||
}
|
this._dealer = context.dealer;
|
||||||
|
|
||||||
private getRepositoryByName<T>(name: string) {
|
|
||||||
return this._repositoryManager.getRepository<T>(name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(request: IGetQuoteUseCaseRequest): Promise<GetQuoteResponseOrError> {
|
async execute(request: IGetQuoteUseCaseRequest): Promise<GetQuoteResponseOrError> {
|
||||||
@ -63,9 +62,7 @@ export class GetQuoteUseCase
|
|||||||
return Result.ok<Quote>(Quote!);
|
return Result.ok<Quote>(Quote!);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const _error = error as IInfrastructureError;
|
const _error = error as IInfrastructureError;
|
||||||
return Result.fail(
|
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Query error", _error));
|
||||||
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al consultar el usuario", _error)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -58,13 +58,7 @@ export class ListQuotesUseCase implements IUseCase<IListQuotesParams, Promise<Li
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const _error = error as IInfrastructureError;
|
const _error = error as IInfrastructureError;
|
||||||
console.trace(_error.message);
|
console.trace(_error.message);
|
||||||
return Result.fail(
|
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Query error", _error));
|
||||||
UseCaseError.create(
|
|
||||||
UseCaseError.REPOSITORY_ERROR,
|
|
||||||
"Error al listar las cotizaciones",
|
|
||||||
_error
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
AggregateRoot,
|
AggregateRoot,
|
||||||
Currency,
|
CurrencyData,
|
||||||
ICollection,
|
ICollection,
|
||||||
IDomainError,
|
IDomainError,
|
||||||
Language,
|
Language,
|
||||||
|
MoneyValue,
|
||||||
Note,
|
Note,
|
||||||
|
Percentage,
|
||||||
Result,
|
Result,
|
||||||
UTCDateValue,
|
UTCDateValue,
|
||||||
UniqueID,
|
UniqueID,
|
||||||
@ -20,13 +22,17 @@ export interface IQuoteProps {
|
|||||||
reference: QuoteReference;
|
reference: QuoteReference;
|
||||||
customer: QuoteCustomer;
|
customer: QuoteCustomer;
|
||||||
language: Language;
|
language: Language;
|
||||||
currency: Currency;
|
currency: CurrencyData;
|
||||||
paymentMethod: Note;
|
paymentMethod: Note;
|
||||||
notes: Note;
|
notes: Note;
|
||||||
validity: Note;
|
validity: Note;
|
||||||
|
|
||||||
items: ICollection<QuoteItem>;
|
items: ICollection<QuoteItem>;
|
||||||
|
|
||||||
|
//subtotalPrice: MoneyValue;
|
||||||
|
discount: Percentage;
|
||||||
|
//totalPrice: MoneyValue;
|
||||||
|
|
||||||
dealerId: UniqueID;
|
dealerId: UniqueID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,10 +44,15 @@ export interface IQuote {
|
|||||||
reference: QuoteReference;
|
reference: QuoteReference;
|
||||||
customer: QuoteCustomer;
|
customer: QuoteCustomer;
|
||||||
language: Language;
|
language: Language;
|
||||||
currency: Currency;
|
currency: CurrencyData;
|
||||||
paymentMethod: Note;
|
paymentMethod: Note;
|
||||||
notes: Note;
|
notes: Note;
|
||||||
validity: Note;
|
validity: Note;
|
||||||
|
|
||||||
|
subtotalPrice: MoneyValue;
|
||||||
|
discount: Percentage;
|
||||||
|
totalPrice: MoneyValue;
|
||||||
|
|
||||||
items: ICollection<QuoteItem>;
|
items: ICollection<QuoteItem>;
|
||||||
|
|
||||||
dealerId: UniqueID;
|
dealerId: UniqueID;
|
||||||
@ -60,6 +71,14 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
|
|||||||
|
|
||||||
protected _items: ICollection<QuoteItem>;
|
protected _items: ICollection<QuoteItem>;
|
||||||
|
|
||||||
|
protected _calculateTotalPriceItems = (): MoneyValue =>
|
||||||
|
this.props.items
|
||||||
|
.toArray()
|
||||||
|
.reduce<MoneyValue>(
|
||||||
|
(accumulator, currentItem) => accumulator.add(currentItem.subtotalPrice),
|
||||||
|
MoneyValue.create({ amount: 0, precision: 2, currencyCode: this.currency.code }).object
|
||||||
|
);
|
||||||
|
|
||||||
protected constructor(props: IQuoteProps, id?: UniqueID) {
|
protected constructor(props: IQuoteProps, id?: UniqueID) {
|
||||||
super(props, id);
|
super(props, id);
|
||||||
|
|
||||||
@ -113,4 +132,16 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
|
|||||||
get dealerId() {
|
get dealerId() {
|
||||||
return this.props.dealerId;
|
return this.props.dealerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get subtotalPrice(): MoneyValue {
|
||||||
|
return this._calculateTotalPriceItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
get discount(): Percentage {
|
||||||
|
return this.props.discount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalPrice(): MoneyValue {
|
||||||
|
return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,21 +4,30 @@ import {
|
|||||||
IDomainError,
|
IDomainError,
|
||||||
IEntityProps,
|
IEntityProps,
|
||||||
MoneyValue,
|
MoneyValue,
|
||||||
|
Percentage,
|
||||||
Quantity,
|
Quantity,
|
||||||
Result,
|
Result,
|
||||||
UniqueID,
|
UniqueID,
|
||||||
} from "@shared/contexts";
|
} from "@shared/contexts";
|
||||||
|
|
||||||
export interface IQuoteItemProps extends IEntityProps {
|
export interface IQuoteItemProps extends IEntityProps {
|
||||||
|
articleId: string;
|
||||||
description: Description; // Descripción del artículo o servicio
|
description: Description; // Descripción del artículo o servicio
|
||||||
quantity: Quantity; // Cantidad de unidades
|
quantity: Quantity; // Cantidad de unidades
|
||||||
unitPrice: MoneyValue; // Precio unitario en la moneda de la factura
|
unitPrice: MoneyValue; // Precio unitario en la moneda de la factura
|
||||||
|
// subtotalPrice: MoneyValue; // Precio unitario * Cantidad
|
||||||
|
discount: Percentage; // % descuento
|
||||||
|
// totalPrice: MoneyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IQuoteItem {
|
export interface IQuoteItem {
|
||||||
|
articleId: string;
|
||||||
description: Description;
|
description: Description;
|
||||||
quantity: Quantity;
|
quantity: Quantity;
|
||||||
unitPrice: MoneyValue;
|
unitPrice: MoneyValue;
|
||||||
|
subtotalPrice: MoneyValue;
|
||||||
|
discount: Percentage;
|
||||||
|
totalPrice: MoneyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QuoteItem extends Entity<IQuoteItemProps> implements IQuoteItem {
|
export class QuoteItem extends Entity<IQuoteItemProps> implements IQuoteItem {
|
||||||
@ -26,6 +35,10 @@ export class QuoteItem extends Entity<IQuoteItemProps> implements IQuoteItem {
|
|||||||
return Result.ok(new QuoteItem(props, id));
|
return Result.ok(new QuoteItem(props, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get articleId(): string {
|
||||||
|
return this.props.articleId;
|
||||||
|
}
|
||||||
|
|
||||||
get description(): Description {
|
get description(): Description {
|
||||||
return this.props.description;
|
return this.props.description;
|
||||||
}
|
}
|
||||||
@ -37,4 +50,16 @@ export class QuoteItem extends Entity<IQuoteItemProps> implements IQuoteItem {
|
|||||||
get unitPrice(): MoneyValue {
|
get unitPrice(): MoneyValue {
|
||||||
return this.props.unitPrice;
|
return this.props.unitPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get subtotalPrice(): MoneyValue {
|
||||||
|
return this.unitPrice.multiply(this.quantity.toNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
get discount(): Percentage {
|
||||||
|
return this.props.discount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalPrice(): MoneyValue {
|
||||||
|
return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
import { ICollection, IGetQuote_Response_DTO } from "@shared/contexts";
|
import {
|
||||||
|
ICollection,
|
||||||
|
IGetQuote_QuoteItem_Response_DTO,
|
||||||
|
IGetQuote_Response_DTO,
|
||||||
|
} from "@shared/contexts";
|
||||||
import { Quote, QuoteItem } from "../../../../../../domain";
|
import { Quote, QuoteItem } from "../../../../../../domain";
|
||||||
import { ISalesContext } from "../../../../../Sales.context";
|
import { ISalesContext } from "../../../../../Sales.context";
|
||||||
|
|
||||||
@ -13,44 +17,38 @@ export const GetQuotePresenter: IGetQuotePresenter = {
|
|||||||
id: quote.id.toString(),
|
id: quote.id.toString(),
|
||||||
status: quote.status.toString(),
|
status: quote.status.toString(),
|
||||||
date: quote.date.toString(),
|
date: quote.date.toString(),
|
||||||
language_code: quote.date.toString(),
|
reference: quote.reference.toString(),
|
||||||
|
customer_information: quote.customer.toString(),
|
||||||
|
lang_code: quote.language.code,
|
||||||
currency_code: quote.currency.toString(),
|
currency_code: quote.currency.toString(),
|
||||||
subtotal: {
|
|
||||||
amount: 0,
|
payment_method: quote.paymentMethod.toString(),
|
||||||
precision: 2,
|
validity: quote.validity.toString(),
|
||||||
currency: "EUR",
|
notes: quote.notes.toString(),
|
||||||
},
|
|
||||||
total: {
|
subtotal_price: quote.subtotalPrice.toObject(),
|
||||||
amount: 0,
|
discount: quote.discount.toObject(),
|
||||||
precision: 2,
|
total_price: quote.totalPrice.toObject(),
|
||||||
currency: "EUR",
|
|
||||||
},
|
|
||||||
items: quoteItemPresenter(quote.items, context),
|
items: quoteItemPresenter(quote.items, context),
|
||||||
|
dealer_id: quote.dealerId.toString(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const quoteItemPresenter = (items: ICollection<QuoteItem>, context: ISalesContext) =>
|
const quoteItemPresenter = (
|
||||||
|
items: ICollection<QuoteItem>,
|
||||||
|
context: ISalesContext
|
||||||
|
): IGetQuote_QuoteItem_Response_DTO[] =>
|
||||||
items.totalCount > 0
|
items.totalCount > 0
|
||||||
? items.items.map((item: QuoteItem) => ({
|
? items.items.map((item: QuoteItem) => ({
|
||||||
|
article_id: item.articleId,
|
||||||
description: item.description.toString(),
|
description: item.description.toString(),
|
||||||
quantity: item.quantity.toString(),
|
quantity: item.quantity.toObject(),
|
||||||
unit_measure: "",
|
unit_price: item.unitPrice.toObject(),
|
||||||
unit_price: {
|
subtotal_price: item.subtotalPrice.toObject(),
|
||||||
amount: 0,
|
discount: item.discount.toObject(),
|
||||||
precision: 2,
|
total_price: item.totalPrice.toObject(),
|
||||||
currency: "EUR",
|
|
||||||
},
|
|
||||||
subtotal: {
|
|
||||||
amount: 0,
|
|
||||||
precision: 2,
|
|
||||||
currency: "EUR",
|
|
||||||
},
|
|
||||||
total: {
|
|
||||||
amount: 0,
|
|
||||||
precision: 2,
|
|
||||||
currency: "EUR",
|
|
||||||
},
|
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { ListQuotesUseCase } from "@/contexts/sales/application";
|
import { ListQuotesUseCase } from "@/contexts/sales/application";
|
||||||
import { registerQuoteRepository } from "@/contexts/sales/infrastructure/Quote.repository";
|
import { registerQuoteRepository } from "@/contexts/sales/infrastructure/Quote.repository";
|
||||||
|
import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context";
|
||||||
import Express from "express";
|
import Express from "express";
|
||||||
import { ListQuotesController } from "./ListQuotes.controller";
|
import { ListQuotesController } from "./ListQuotes.controller";
|
||||||
import { ListQuotesPresenter } from "./presenter";
|
import { ListQuotesPresenter } from "./presenter";
|
||||||
@ -9,7 +10,7 @@ export const listQuotesController = (
|
|||||||
res: Express.Response,
|
res: Express.Response,
|
||||||
next: Express.NextFunction
|
next: Express.NextFunction
|
||||||
) => {
|
) => {
|
||||||
const context = res.locals.context;
|
const context: ISalesContext = res.locals.context;
|
||||||
|
|
||||||
registerQuoteRepository(context);
|
registerQuoteRepository(context);
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Currency, Language, Note, UTCDateValue, UniqueID } from "@shared/contexts";
|
import { CurrencyData, Language, Note, Percentage, UTCDateValue, UniqueID } from "@shared/contexts";
|
||||||
|
|
||||||
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
|
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
|
||||||
import { IQuoteProps, Quote, QuoteCustomer, QuoteReference } from "../../domain";
|
import { IQuoteProps, Quote, QuoteCustomer, QuoteReference } from "../../domain";
|
||||||
@ -33,18 +33,41 @@ class QuoteMapper
|
|||||||
|
|
||||||
const props: IQuoteProps = {
|
const props: IQuoteProps = {
|
||||||
status: this.mapsValue(source, "status", QuoteStatus.create),
|
status: this.mapsValue(source, "status", QuoteStatus.create),
|
||||||
date: this.mapsValue(source, "issue_date", UTCDateValue.create),
|
date: this.mapsValue(source, "date", UTCDateValue.create),
|
||||||
reference: this.mapsValue(source, "reference", QuoteReference.create),
|
reference: this.mapsValue(source, "reference", QuoteReference.create),
|
||||||
currency: this.mapsValue(source, "quote_currency", Currency.createFromCode),
|
currency: this.mapsValue(source, "currency_code", CurrencyData.createFromCode),
|
||||||
language: this.mapsValue(source, "quote_language", Language.createFromCode),
|
language: this.mapsValue(source, "lang_code", Language.createFromCode),
|
||||||
customer: this.mapsValue(source, "customer", QuoteCustomer.create),
|
customer: this.mapsValue(source, "customer_information", QuoteCustomer.create),
|
||||||
|
|
||||||
validity: this.mapsValue(source, "validity", Note.create),
|
validity: this.mapsValue(source, "validity", Note.create),
|
||||||
paymentMethod: this.mapsValue(source, "paymentMethod", Note.create),
|
paymentMethod: this.mapsValue(source, "payment_method", Note.create),
|
||||||
notes: this.mapsValue(source, "notes", Note.create),
|
notes: this.mapsValue(source, "notes", Note.create),
|
||||||
|
|
||||||
items,
|
items,
|
||||||
|
|
||||||
|
/*subtotal: this.mapsValue(source, "subtotal_price", (subtotal_price) =>
|
||||||
|
MoneyValue.create({
|
||||||
|
amount: subtotal_price,
|
||||||
|
currencyCode: source.currency_code,
|
||||||
|
precision: 2,
|
||||||
|
})
|
||||||
|
),*/
|
||||||
|
|
||||||
|
discount: this.mapsValue(source, "discount", (discount) =>
|
||||||
|
Percentage.create({
|
||||||
|
amount: discount,
|
||||||
|
precision: Percentage.DEFAULT_PRECISION,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
/*totalPrice: this.mapsValue(source, "total_price", (total_price) =>
|
||||||
|
MoneyValue.create({
|
||||||
|
amount: total_price,
|
||||||
|
currencyCode: source.currency_code,
|
||||||
|
precision: 2,
|
||||||
|
})
|
||||||
|
),*/
|
||||||
|
|
||||||
dealerId: this.mapsValue(source, "dealer_id", UniqueID.create),
|
dealerId: this.mapsValue(source, "dealer_id", UniqueID.create),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -75,9 +98,9 @@ class QuoteMapper
|
|||||||
payment_method: source.paymentMethod.toPrimitive(),
|
payment_method: source.paymentMethod.toPrimitive(),
|
||||||
notes: source.notes.toPrimitive(),
|
notes: source.notes.toPrimitive(),
|
||||||
|
|
||||||
discount: 0,
|
subtotal_price: source.subtotalPrice.toPrimitive(),
|
||||||
subtotal: 0,
|
discount: source.discount.toPrimitive(),
|
||||||
total: 0,
|
total_price: source.totalPrice.toPrimitive(),
|
||||||
|
|
||||||
items,
|
items,
|
||||||
dealer_id: source.dealerId.toPrimitive(),
|
dealer_id: source.dealerId.toPrimitive(),
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
|
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
|
||||||
import { Description, Quantity, UniqueID, UnitPrice } from "@shared/contexts";
|
import { Description, MoneyValue, Percentage, Quantity, UniqueID } from "@shared/contexts";
|
||||||
import { IQuoteItemProps, Quote, QuoteItem } from "../../domain";
|
import { IQuoteItemProps, Quote, QuoteItem } from "../../domain";
|
||||||
import { ISalesContext } from "../Sales.context";
|
import { ISalesContext } from "../Sales.context";
|
||||||
import { Quote_Model } from "../sequelize";
|
import { Quote_Model } from "../sequelize";
|
||||||
@ -23,15 +23,40 @@ class QuoteItemMapper
|
|||||||
const id = this.mapsValue(source, "item_id", UniqueID.create);
|
const id = this.mapsValue(source, "item_id", UniqueID.create);
|
||||||
|
|
||||||
const props: IQuoteItemProps = {
|
const props: IQuoteItemProps = {
|
||||||
|
articleId: source.id_article,
|
||||||
description: this.mapsValue(source, "description", Description.create),
|
description: this.mapsValue(source, "description", Description.create),
|
||||||
quantity: this.mapsValue(source, "quantity", Quantity.create),
|
quantity: this.mapsValue(source, "quantity", Quantity.create),
|
||||||
unitPrice: this.mapsValue(source, "unit_price", (unit_price) =>
|
unitPrice: this.mapsValue(source, "unit_price", (unit_price) =>
|
||||||
UnitPrice.create({
|
MoneyValue.create({
|
||||||
amount: unit_price,
|
amount: unit_price,
|
||||||
currencyCode: sourceParent.currency_code,
|
currencyCode: sourceParent.currency_code,
|
||||||
precision: 4,
|
precision: 4,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/*subtotalPrice: this.mapsValue(source, "subtotal_price", (subtotal_price) =>
|
||||||
|
MoneyValue.create({
|
||||||
|
amount: subtotal_price,
|
||||||
|
currencyCode: sourceParent.currency_code,
|
||||||
|
precision: 4,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
*/
|
||||||
|
|
||||||
|
discount: this.mapsValue(source, "discount", (discount) =>
|
||||||
|
Percentage.create({
|
||||||
|
amount: discount,
|
||||||
|
precision: Percentage.DEFAULT_PRECISION,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
/*totalPrice: this.mapsValue(source, "total_price", (total_price) =>
|
||||||
|
MoneyValue.create({
|
||||||
|
amount: total_price,
|
||||||
|
currencyCode: sourceParent.currency_code,
|
||||||
|
precision: 2,
|
||||||
|
})
|
||||||
|
),*/
|
||||||
};
|
};
|
||||||
|
|
||||||
const quoteItemOrError = QuoteItem.create(props, id);
|
const quoteItemOrError = QuoteItem.create(props, id);
|
||||||
@ -50,16 +75,16 @@ class QuoteItemMapper
|
|||||||
const { index, sourceParent } = params;
|
const { index, sourceParent } = params;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
item_id: source.id.toString(),
|
||||||
quote_id: sourceParent.id.toPrimitive(),
|
quote_id: sourceParent.id.toPrimitive(),
|
||||||
position: index,
|
position: index,
|
||||||
item_id: "", //article_id: source.id.toPrimitive(),
|
id_article: source.articleId,
|
||||||
description: source.description.toPrimitive(),
|
description: source.description.toPrimitive(),
|
||||||
quantity: source.quantity.toPrimitive(),
|
quantity: source.quantity.toPrimitive(),
|
||||||
unit_price: source.unitPrice.toPrimitive(),
|
unit_price: source.unitPrice.toPrimitive(),
|
||||||
subtotal: 0,
|
subtotal_price: source.subtotalPrice.toPrimitive(),
|
||||||
total: 0,
|
discount: source.discount.toPrimitive(),
|
||||||
//subtotal: source.calculateSubtotal().toPrimitive(),
|
total_price: source.totalPrice.toPrimitive(),
|
||||||
//total: source.calculateTotal().toPrimitive(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,9 +50,9 @@ export class Quote_Model extends Model<
|
|||||||
declare notes: CreationOptional<string>;
|
declare notes: CreationOptional<string>;
|
||||||
declare validity: CreationOptional<string>;
|
declare validity: CreationOptional<string>;
|
||||||
|
|
||||||
declare subtotal: CreationOptional<number>;
|
declare subtotal_price: CreationOptional<number>;
|
||||||
declare discount: CreationOptional<number>;
|
declare discount: CreationOptional<number>;
|
||||||
declare total: CreationOptional<number>;
|
declare total_price: CreationOptional<number>;
|
||||||
|
|
||||||
declare items: NonAttribute<QuoteItem_Model[]>;
|
declare items: NonAttribute<QuoteItem_Model[]>;
|
||||||
declare dealer: NonAttribute<Dealer_Model>;
|
declare dealer: NonAttribute<Dealer_Model>;
|
||||||
@ -108,7 +108,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
},
|
},
|
||||||
|
|
||||||
subtotal: {
|
subtotal_price: {
|
||||||
type: new DataTypes.BIGINT(),
|
type: new DataTypes.BIGINT(),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
@ -118,7 +118,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
total: {
|
total_price: {
|
||||||
type: new DataTypes.BIGINT(),
|
type: new DataTypes.BIGINT(),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -30,12 +30,14 @@ export class QuoteItem_Model extends Model<
|
|||||||
|
|
||||||
declare quote_id: string;
|
declare quote_id: string;
|
||||||
declare item_id: string;
|
declare item_id: string;
|
||||||
|
declare id_article: string; // number ??
|
||||||
declare position: number;
|
declare position: number;
|
||||||
declare description: CreationOptional<string>;
|
declare description: CreationOptional<string>;
|
||||||
declare quantity: CreationOptional<number>;
|
declare quantity: CreationOptional<number>;
|
||||||
declare unit_price: CreationOptional<number>;
|
declare unit_price: CreationOptional<number>;
|
||||||
declare subtotal: CreationOptional<number>;
|
declare subtotal_price: CreationOptional<number>;
|
||||||
declare total: CreationOptional<number>;
|
declare discount: CreationOptional<number>;
|
||||||
|
declare total_price: CreationOptional<number>;
|
||||||
|
|
||||||
declare quote: NonAttribute<Quote_Model>;
|
declare quote: NonAttribute<Quote_Model>;
|
||||||
}
|
}
|
||||||
@ -51,6 +53,10 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: new DataTypes.UUID(),
|
type: new DataTypes.UUID(),
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
},
|
},
|
||||||
|
id_article: {
|
||||||
|
type: DataTypes.BIGINT().UNSIGNED,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
position: {
|
position: {
|
||||||
type: new DataTypes.MEDIUMINT(),
|
type: new DataTypes.MEDIUMINT(),
|
||||||
autoIncrement: false,
|
autoIncrement: false,
|
||||||
@ -68,11 +74,15 @@ export default (sequelize: Sequelize) => {
|
|||||||
type: new DataTypes.BIGINT(),
|
type: new DataTypes.BIGINT(),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
subtotal: {
|
subtotal_price: {
|
||||||
type: new DataTypes.BIGINT(),
|
type: new DataTypes.BIGINT(),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
total: {
|
discount: {
|
||||||
|
type: DataTypes.BIGINT(),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
total_price: {
|
||||||
type: new DataTypes.BIGINT(),
|
type: new DataTypes.BIGINT(),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { checkUser } from "@/contexts/auth";
|
import { checkUser } from "@/contexts/auth";
|
||||||
import {
|
import {
|
||||||
createQuoteController,
|
createQuoteController,
|
||||||
|
getQuoteController,
|
||||||
listQuotesController,
|
listQuotesController,
|
||||||
|
updateQuoteController,
|
||||||
} from "@/contexts/sales/infrastructure/express/controllers";
|
} from "@/contexts/sales/infrastructure/express/controllers";
|
||||||
import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware";
|
import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware";
|
||||||
import Express from "express";
|
import Express from "express";
|
||||||
@ -10,11 +12,11 @@ export const QuoteRouter = (appRouter: Express.Router) => {
|
|||||||
const quoteRoutes: Express.Router = Express.Router({ mergeParams: true });
|
const quoteRoutes: Express.Router = Express.Router({ mergeParams: true });
|
||||||
|
|
||||||
quoteRoutes.get("/", checkUser, getDealerMiddleware, listQuotesController);
|
quoteRoutes.get("/", checkUser, getDealerMiddleware, listQuotesController);
|
||||||
|
quoteRoutes.get("/:quoteId", checkUser, getDealerMiddleware, getQuoteController);
|
||||||
quoteRoutes.post("/", checkUser, getDealerMiddleware, createQuoteController);
|
quoteRoutes.post("/", checkUser, getDealerMiddleware, createQuoteController);
|
||||||
|
quoteRoutes.put("/:quoteId", checkUser, updateQuoteController);
|
||||||
|
|
||||||
//quoteRoutes.put("/:quoteId", checkUser, updateQuoteController);
|
/*
|
||||||
|
|
||||||
/*quoteRoutes.get("/:quoteId", isUser, getQuoteMiddleware, getQuoteController);
|
|
||||||
quoteRoutes.post("/", isAdmin, createQuoteController);
|
quoteRoutes.post("/", isAdmin, createQuoteController);
|
||||||
|
|
||||||
quoteRoutes.delete("/:quoteId", isAdmin, deleteQuoteController);*/
|
quoteRoutes.delete("/:quoteId", isAdmin, deleteQuoteController);*/
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { IMoney_Response_DTO } from "../../../../common";
|
import { IQuantuty_Response_DTO } from "../../../../common";
|
||||||
|
|
||||||
export interface IListArticles_Response_DTO {
|
export interface IListArticles_Response_DTO {
|
||||||
id: string;
|
id: string;
|
||||||
@ -10,5 +10,5 @@ export interface IListArticles_Response_DTO {
|
|||||||
|
|
||||||
description: string;
|
description: string;
|
||||||
points: number;
|
points: number;
|
||||||
retail_price: IMoney_Response_DTO;
|
retail_price: IQuantuty_Response_DTO;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Currency,
|
CurrencyData,
|
||||||
Description,
|
Description,
|
||||||
Email,
|
Email,
|
||||||
Language,
|
Language,
|
||||||
@ -46,7 +46,7 @@ export const ensureDateIsValid = (value: string): Result<boolean, Error> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ensureCurrencyCodeIsValid = (value: string): Result<boolean, Error> => {
|
export const ensureCurrencyCodeIsValid = (value: string): Result<boolean, Error> => {
|
||||||
const currencyOrError = Currency.createFromCode(value);
|
const currencyOrError = CurrencyData.createFromCode(value);
|
||||||
|
|
||||||
return currencyOrError.isSuccess ? Result.ok(true) : Result.fail(currencyOrError.error);
|
return currencyOrError.isSuccess ? Result.ok(true) : Result.fail(currencyOrError.error);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,14 +4,14 @@ import { Result, RuleValidator } from "../../domain";
|
|||||||
export interface IMoney_Request_DTO {
|
export interface IMoney_Request_DTO {
|
||||||
amount: number;
|
amount: number;
|
||||||
precision: number;
|
precision: number;
|
||||||
currency: string;
|
currency_code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureMoney_DTOIsValid(money: IMoney_Request_DTO) {
|
export function ensureMoney_DTOIsValid(money: IMoney_Request_DTO) {
|
||||||
const schema = Joi.object({
|
const schema = Joi.object({
|
||||||
amount: Joi.number(),
|
amount: Joi.number(),
|
||||||
precision: Joi.number(),
|
precision: Joi.number(),
|
||||||
currency: Joi.string(),
|
currency_code: Joi.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = RuleValidator.validate<IMoney_Request_DTO>(schema, money);
|
const result = RuleValidator.validate<IMoney_Request_DTO>(schema, money);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export interface IPercentage_DTO {
|
export interface IPercentage_DTO {
|
||||||
amount: number;
|
amount: number;
|
||||||
precision: number;
|
precision: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPercentage_Request_DTO extends IPercentage_DTO {}
|
||||||
export interface IPercentage_Response_DTO extends IPercentage_DTO {}
|
export interface IPercentage_Response_DTO extends IPercentage_DTO {}
|
||||||
|
|||||||
24
shared/lib/contexts/common/application/dto/IQuantity.dto.ts
Normal file
24
shared/lib/contexts/common/application/dto/IQuantity.dto.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import Joi from "joi";
|
||||||
|
import { Result, RuleValidator } from "../../domain";
|
||||||
|
|
||||||
|
export interface IQuantity_Request_DTO {
|
||||||
|
amount: number;
|
||||||
|
precision: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureQuantity_DTOIsValid(quantity: IQuantity_Request_DTO) {
|
||||||
|
const schema = Joi.object({
|
||||||
|
amount: Joi.number(),
|
||||||
|
precision: Joi.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = RuleValidator.validate<IQuantity_Request_DTO>(schema, quantity);
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Result.fail(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IQuantity_Response_DTO extends IQuantity_Request_DTO {}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./IError_Response.dto";
|
export * from "./IError_Response.dto";
|
||||||
export * from "./IMoney.dto";
|
export * from "./IMoney.dto";
|
||||||
export * from "./IPercentage.dto";
|
export * from "./IPercentage.dto";
|
||||||
|
export * from "./IQuantity.dto";
|
||||||
export * from "./ITaxType.dto";
|
export * from "./ITaxType.dto";
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
export interface ICollection<T> {
|
export interface ICollection<T> {
|
||||||
items: T[];
|
items: T[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
|
|
||||||
|
toArray(): T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Collection<T> implements ICollection<T> {
|
export class Collection<T> implements ICollection<T> {
|
||||||
@ -15,7 +17,7 @@ export class Collection<T> implements ICollection<T> {
|
|||||||
|
|
||||||
return Array.from(this._items.values()).reduce(
|
return Array.from(this._items.values()).reduce(
|
||||||
(total, item) => (item !== undefined ? total + 1 : total),
|
(total, item) => (item !== undefined ? total + 1 : total),
|
||||||
0,
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,10 +28,8 @@ export class Collection<T> implements ICollection<T> {
|
|||||||
constructor(initialValues?: T[], totalCount?: number) {
|
constructor(initialValues?: T[], totalCount?: number) {
|
||||||
this._items = new Map<number, T>(
|
this._items = new Map<number, T>(
|
||||||
initialValues
|
initialValues
|
||||||
? initialValues.map(
|
? initialValues.map((value: any, index: number) => [index, value] as [number, T])
|
||||||
(value: any, index: number) => [index, value] as [number, T],
|
: []
|
||||||
)
|
|
||||||
: [],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this._totalCountIsProvided = typeof totalCount === "number";
|
this._totalCountIsProvided = typeof totalCount === "number";
|
||||||
@ -89,9 +89,7 @@ export class Collection<T> implements ICollection<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public find(
|
public find(predicate: (value: T, index: number, obj: T[]) => unknown): T | undefined {
|
||||||
predicate: (value: T, index: number, obj: T[]) => unknown,
|
|
||||||
): T | undefined {
|
|
||||||
return Array.from(this._items.values()).find(predicate);
|
return Array.from(this._items.values()).find(predicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,8 +104,8 @@ export class Collection<T> implements ICollection<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public toStringArray(): string[] {
|
public toStringArray(): string[] {
|
||||||
return Array.from(this._items.values(), (element) =>
|
return Array.from(this._items.values(), (element) => JSON.stringify(element)).filter(
|
||||||
JSON.stringify(element),
|
(element) => element.length > 0
|
||||||
).filter((element) => element.length > 0);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./Currency";
|
|
||||||
@ -5,7 +5,7 @@ import { DomainError, handleDomainError } from "../../errors";
|
|||||||
import { INullableValueObjectOptions, NullableValueObject } from "../NullableValueObject";
|
import { INullableValueObjectOptions, NullableValueObject } from "../NullableValueObject";
|
||||||
import { Result } from "../Result";
|
import { Result } from "../Result";
|
||||||
|
|
||||||
export interface ICurrency {
|
export interface ICurrencyData {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
name: string;
|
name: string;
|
||||||
symbol_native: string;
|
symbol_native: string;
|
||||||
@ -15,9 +15,9 @@ export interface ICurrency {
|
|||||||
name_plural: string;
|
name_plural: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICurrencyOptions extends INullableValueObjectOptions {}
|
export interface ICurrencyDataOptions extends INullableValueObjectOptions {}
|
||||||
|
|
||||||
export class Currency extends NullableValueObject<ICurrency> {
|
export class CurrencyData extends NullableValueObject<ICurrencyData> {
|
||||||
public static readonly DEFAULT_CURRENCY_CODE = "EUR";
|
public static readonly DEFAULT_CURRENCY_CODE = "EUR";
|
||||||
public static readonly CURRENCIES = Currencies;
|
public static readonly CURRENCIES = Currencies;
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ export class Currency extends NullableValueObject<ICurrency> {
|
|||||||
return this.props ? String(this.props.code) : "";
|
return this.props ? String(this.props.code) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static validate(value: string, options: ICurrencyOptions) {
|
protected static validate(value: string, options: ICurrencyDataOptions) {
|
||||||
const rule = Joi.alternatives(
|
const rule = Joi.alternatives(
|
||||||
RuleValidator.RULE_ALLOW_EMPTY.default(""),
|
RuleValidator.RULE_ALLOW_EMPTY.default(""),
|
||||||
Joi.string()
|
Joi.string()
|
||||||
@ -41,24 +41,24 @@ export class Currency extends NullableValueObject<ICurrency> {
|
|||||||
return RuleValidator.validate<string>(rule, value);
|
return RuleValidator.validate<string>(rule, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createFromCode(currencyCode: string, options: ICurrencyOptions = {}) {
|
public static createFromCode(currencyCode: string, options: ICurrencyDataOptions = {}) {
|
||||||
const _options = {
|
const _options = {
|
||||||
...options,
|
...options,
|
||||||
label: options.label ? options.label : "current_code",
|
label: options.label ? options.label : "current_code",
|
||||||
};
|
};
|
||||||
|
|
||||||
const validationResult = Currency.validate(currencyCode, _options);
|
const validationResult = CurrencyData.validate(currencyCode, _options);
|
||||||
|
|
||||||
if (validationResult.isFailure) {
|
if (validationResult.isFailure) {
|
||||||
return Result.fail(
|
return Result.fail(
|
||||||
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
handleDomainError(DomainError.INVALID_INPUT_DATA, validationResult.error.message, _options)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Result.ok(new Currency(Currencies[validationResult.object]));
|
return Result.ok(new CurrencyData(Currencies[validationResult.object]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createDefaultCode() {
|
public static createDefaultCode() {
|
||||||
return Currency.createFromCode(Currency.DEFAULT_CURRENCY_CODE);
|
return CurrencyData.createFromCode(CurrencyData.DEFAULT_CURRENCY_CODE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isEmpty(): boolean {
|
public isEmpty(): boolean {
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./CurrencyData";
|
||||||
@ -1,27 +1,28 @@
|
|||||||
/* eslint-disable no-use-before-define */
|
/* eslint-disable no-use-before-define */
|
||||||
import DineroFactory, { Dinero } from "dinero.js";
|
import DineroFactory, { Dinero } from "dinero.js";
|
||||||
|
|
||||||
import { Currency } from "./Currency";
|
|
||||||
|
|
||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import { isNull } from "lodash";
|
import { isNull } from "lodash";
|
||||||
import { NullOr } from "../../../../utilities";
|
import { NullOr } from "../../../../utilities";
|
||||||
import { RuleValidator } from "../RuleValidator";
|
import { RuleValidator } from "../RuleValidator";
|
||||||
|
import { CurrencyData } from "./CurrencyData";
|
||||||
import { Result } from "./Result";
|
import { Result } from "./Result";
|
||||||
import { IValueObjectOptions, ValueObject } from "./ValueObject";
|
import { IValueObjectOptions, ValueObject } from "./ValueObject";
|
||||||
|
|
||||||
export interface IMoneyValueOptions extends IValueObjectOptions {
|
export interface IMoneyValueOptions extends IValueObjectOptions {
|
||||||
|
defaultValue?: number;
|
||||||
locale: string;
|
locale: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultMoneyValueOptions: IMoneyValueOptions = {
|
export const defaultMoneyValueOptions: IMoneyValueOptions = {
|
||||||
|
defaultValue: 0,
|
||||||
locale: "es-ES",
|
locale: "es-ES",
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MoneyValueObject {
|
export interface MoneyValueObject {
|
||||||
amount: number;
|
amount: number;
|
||||||
precision: number;
|
precision: number;
|
||||||
currency: string;
|
currency_code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoundingMode =
|
type RoundingMode =
|
||||||
@ -43,7 +44,7 @@ export interface IMoneyValueProps {
|
|||||||
|
|
||||||
const defaultMoneyValueProps = {
|
const defaultMoneyValueProps = {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
currencyCode: Currency.DEFAULT_CURRENCY_CODE,
|
currencyCode: CurrencyData.DEFAULT_CURRENCY_CODE,
|
||||||
precision: 2,
|
precision: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,13 +57,10 @@ interface IMoneyValue {
|
|||||||
isNull(): boolean;
|
isNull(): boolean;
|
||||||
|
|
||||||
getAmount(): number;
|
getAmount(): number;
|
||||||
getCurrency(): Currency;
|
getCurrency(): CurrencyData;
|
||||||
getLocale(): string;
|
getLocale(): string;
|
||||||
getPrecision(): number;
|
getPrecision(): number;
|
||||||
convertPrecision(
|
convertPrecision(newPrecision: number, roundingMode?: RoundingMode): MoneyValue;
|
||||||
newPrecision: number,
|
|
||||||
roundingMode?: RoundingMode,
|
|
||||||
): MoneyValue;
|
|
||||||
|
|
||||||
add(addend: MoneyValue): MoneyValue;
|
add(addend: MoneyValue): MoneyValue;
|
||||||
subtract(subtrahend: MoneyValue): MoneyValue;
|
subtract(subtrahend: MoneyValue): MoneyValue;
|
||||||
@ -90,16 +88,16 @@ interface IMoneyValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||||
|
public static readonly DEFAULT_PRECISION = defaultMoneyValueProps.precision;
|
||||||
|
public static readonly DEFAULT_CURRENCY_CODE = defaultMoneyValueProps.currencyCode;
|
||||||
|
|
||||||
private static readonly MIN_VALUE = Number.MIN_VALUE;
|
private static readonly MIN_VALUE = Number.MIN_VALUE;
|
||||||
private static readonly MAX_VALUE = Number.MAX_VALUE;
|
private static readonly MAX_VALUE = Number.MAX_VALUE;
|
||||||
|
|
||||||
private readonly _isNull: boolean;
|
private readonly _isNull: boolean;
|
||||||
private readonly _options: IMoneyValueOptions;
|
private readonly _options: IMoneyValueOptions;
|
||||||
|
|
||||||
protected static validate(
|
protected static validate(amount: NullOr<number | string>, options: IMoneyValueOptions) {
|
||||||
amount: NullOr<number | string>,
|
|
||||||
options: IMoneyValueOptions,
|
|
||||||
) {
|
|
||||||
const ruleNull = Joi.any()
|
const ruleNull = Joi.any()
|
||||||
.optional() // <- undefined
|
.optional() // <- undefined
|
||||||
.valid(null); // <- null
|
.valid(null); // <- null
|
||||||
@ -131,7 +129,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
|||||||
|
|
||||||
public static create(
|
public static create(
|
||||||
props: IMoneyValueProps = defaultMoneyValueProps,
|
props: IMoneyValueProps = defaultMoneyValueProps,
|
||||||
options = defaultMoneyValueOptions,
|
options = defaultMoneyValueOptions
|
||||||
) {
|
) {
|
||||||
if (props === null) {
|
if (props === null) {
|
||||||
throw new Error(`InvalidParams: props params is missing`);
|
throw new Error(`InvalidParams: props params is missing`);
|
||||||
@ -141,7 +139,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
|||||||
amount = defaultMoneyValueProps.amount,
|
amount = defaultMoneyValueProps.amount,
|
||||||
currencyCode = defaultMoneyValueProps.currencyCode,
|
currencyCode = defaultMoneyValueProps.currencyCode,
|
||||||
precision = defaultMoneyValueProps.precision,
|
precision = defaultMoneyValueProps.precision,
|
||||||
} = props;
|
} = props || {};
|
||||||
|
|
||||||
const validationResult = MoneyValue.validate(amount, options);
|
const validationResult = MoneyValue.validate(amount, options);
|
||||||
|
|
||||||
@ -149,13 +147,11 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
|||||||
return Result.fail(validationResult.error);
|
return Result.fail(validationResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _amount: NullOr<number> = MoneyValue.sanitize(
|
const _amount: NullOr<number> = MoneyValue.sanitize(validationResult.object);
|
||||||
validationResult.object,
|
|
||||||
);
|
|
||||||
|
|
||||||
const prop = DineroFactory({
|
const prop = DineroFactory({
|
||||||
amount: !isNull(_amount) ? _amount : 0,
|
amount: !isNull(_amount) ? _amount : options.defaultValue,
|
||||||
currency: Currency.DEFAULT_CURRENCY_CODE,
|
currency: CurrencyData.createFromCode(currencyCode).object.code,
|
||||||
precision,
|
precision,
|
||||||
}).setLocale(options.locale);
|
}).setLocale(options.locale);
|
||||||
|
|
||||||
@ -175,29 +171,23 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected static createFromDinero(dinero: Dinero) {
|
protected static createFromDinero(dinero: Dinero) {
|
||||||
return Result.ok<MoneyValue>(
|
return Result.ok<MoneyValue>(new MoneyValue(dinero, false, defaultMoneyValueOptions));
|
||||||
new MoneyValue(dinero, false, defaultMoneyValueOptions),
|
}
|
||||||
|
|
||||||
|
public static normalizePrecision(objects: ReadonlyArray<MoneyValue>): MoneyValue[] {
|
||||||
|
return DineroFactory.normalizePrecision(objects.map((object) => object.props)).map(
|
||||||
|
(dinero) => MoneyValue.createFromDinero(dinero).object
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static normalizePrecision(
|
|
||||||
objects: ReadonlyArray<MoneyValue>,
|
|
||||||
): MoneyValue[] {
|
|
||||||
return DineroFactory.normalizePrecision(
|
|
||||||
objects.map((object) => object.props),
|
|
||||||
).map((dinero) => MoneyValue.createFromDinero(dinero).object);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static minimum(objects: ReadonlyArray<MoneyValue>): MoneyValue {
|
public static minimum(objects: ReadonlyArray<MoneyValue>): MoneyValue {
|
||||||
return MoneyValue.createFromDinero(
|
return MoneyValue.createFromDinero(DineroFactory.minimum(objects.map((object) => object.props)))
|
||||||
DineroFactory.minimum(objects.map((object) => object.props)),
|
.object;
|
||||||
).object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static maximum(objects: ReadonlyArray<MoneyValue>): MoneyValue {
|
public static maximum(objects: ReadonlyArray<MoneyValue>): MoneyValue {
|
||||||
return MoneyValue.createFromDinero(
|
return MoneyValue.createFromDinero(DineroFactory.maximum(objects.map((object) => object.props)))
|
||||||
DineroFactory.maximum(objects.map((object) => object.props)),
|
.object;
|
||||||
).object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(value: Dinero, isNull: boolean, options: IMoneyValueOptions) {
|
constructor(value: Dinero, isNull: boolean, options: IMoneyValueOptions) {
|
||||||
@ -238,17 +228,13 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
|||||||
return this.props.getPrecision();
|
return this.props.getPrecision();
|
||||||
}
|
}
|
||||||
|
|
||||||
public convertPrecision(
|
public convertPrecision(newPrecision: number, roundingMode?: RoundingMode): MoneyValue {
|
||||||
newPrecision: number,
|
return MoneyValue.createFromDinero(this.props.convertPrecision(newPrecision, roundingMode))
|
||||||
roundingMode?: RoundingMode,
|
.object;
|
||||||
): MoneyValue {
|
|
||||||
return MoneyValue.createFromDinero(
|
|
||||||
this.props.convertPrecision(newPrecision, roundingMode),
|
|
||||||
).object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCurrency(): Currency {
|
public getCurrency(): CurrencyData {
|
||||||
return Currency.createFromCode(this.props.getCurrency()).object;
|
return CurrencyData.createFromCode(this.props.getCurrency()).object;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLocale(): string {
|
public getLocale(): string {
|
||||||
@ -260,34 +246,23 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public subtract(subtrahend: MoneyValue): MoneyValue {
|
public subtract(subtrahend: MoneyValue): MoneyValue {
|
||||||
return MoneyValue.createFromDinero(this.props.subtract(subtrahend.props))
|
return MoneyValue.createFromDinero(this.props.subtract(subtrahend.props)).object;
|
||||||
.object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public multiply(multiplier: number, roundingMode?: RoundingMode): MoneyValue {
|
public multiply(multiplier: number, roundingMode?: RoundingMode): MoneyValue {
|
||||||
return MoneyValue.createFromDinero(
|
return MoneyValue.createFromDinero(this.props.multiply(multiplier, roundingMode)).object;
|
||||||
this.props.multiply(multiplier, roundingMode),
|
|
||||||
).object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public divide(divisor: number, roundingMode?: RoundingMode): MoneyValue {
|
public divide(divisor: number, roundingMode?: RoundingMode): MoneyValue {
|
||||||
return MoneyValue.createFromDinero(this.props.divide(divisor, roundingMode))
|
return MoneyValue.createFromDinero(this.props.divide(divisor, roundingMode)).object;
|
||||||
.object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public percentage(
|
public percentage(percentage: number, roundingMode?: RoundingMode): MoneyValue {
|
||||||
percentage: number,
|
return MoneyValue.createFromDinero(this.props.percentage(percentage, roundingMode)).object;
|
||||||
roundingMode?: RoundingMode,
|
|
||||||
): MoneyValue {
|
|
||||||
return MoneyValue.createFromDinero(
|
|
||||||
this.props.percentage(percentage, roundingMode),
|
|
||||||
).object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public allocate(ratios: ReadonlyArray<number>): MoneyValue[] {
|
public allocate(ratios: ReadonlyArray<number>): MoneyValue[] {
|
||||||
return this.props
|
return this.props.allocate(ratios).map((dinero) => MoneyValue.createFromDinero(dinero).object);
|
||||||
.allocate(ratios)
|
|
||||||
.map((dinero) => MoneyValue.createFromDinero(dinero).object);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public equalsTo(comparator: MoneyValue): boolean {
|
public equalsTo(comparator: MoneyValue): boolean {
|
||||||
@ -347,7 +322,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
|||||||
return {
|
return {
|
||||||
amount: obj.amount,
|
amount: obj.amount,
|
||||||
precision: obj.precision,
|
precision: obj.precision,
|
||||||
currency: String(obj.currency),
|
currency_code: String(obj.currency),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,57 +1,160 @@
|
|||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
|
import { isNull } from "lodash";
|
||||||
|
import { NullOr } from "../../../../utilities";
|
||||||
import { RuleValidator } from "../RuleValidator";
|
import { RuleValidator } from "../RuleValidator";
|
||||||
import {
|
import { INullableValueObjectOptions, NullableValueObject } from "./NullableValueObject";
|
||||||
INullableValueObjectOptions,
|
|
||||||
NullableValueObject,
|
|
||||||
} from "./NullableValueObject";
|
|
||||||
import { Result } from "./Result";
|
import { Result } from "./Result";
|
||||||
|
|
||||||
export class Percentage extends NullableValueObject<number> {
|
export interface IPercentageOptions extends INullableValueObjectOptions {}
|
||||||
private static readonly MIN_VALUE = 0;
|
|
||||||
private static readonly MAX_VALUE = 100;
|
export interface IPercentageProps {
|
||||||
|
amount: NullOr<number | string>;
|
||||||
|
precision?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPercentage {
|
||||||
|
amount: NullOr<number>;
|
||||||
|
precision: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PercentageObject {
|
||||||
|
amount: number;
|
||||||
|
precision: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPercentageProps = {
|
||||||
|
amount: 0,
|
||||||
|
precision: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Percentage extends NullableValueObject<IPercentage> {
|
||||||
|
public static readonly DEFAULT_PRECISION = 2;
|
||||||
|
public static readonly MIN_VALUE = 0;
|
||||||
|
public static readonly MAX_VALUE = 100;
|
||||||
|
|
||||||
|
private readonly _isNull: boolean;
|
||||||
|
private readonly _options: IPercentageOptions;
|
||||||
|
|
||||||
|
protected static validate(value: NullOr<number | string>, options: IPercentageOptions) {
|
||||||
|
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(
|
||||||
|
defaultPercentageProps.amount
|
||||||
|
);
|
||||||
|
|
||||||
protected static validate(
|
|
||||||
value: number,
|
|
||||||
options: INullableValueObjectOptions,
|
|
||||||
) {
|
|
||||||
const rule = Joi.number()
|
const rule = Joi.number()
|
||||||
.min(Percentage.MIN_VALUE)
|
.min(Percentage.MIN_VALUE)
|
||||||
.max(Percentage.MAX_VALUE)
|
.max(Percentage.MAX_VALUE)
|
||||||
|
|
||||||
.label(options.label ? options.label : "value");
|
.label(options.label ? options.label : "percentage");
|
||||||
|
|
||||||
return RuleValidator.validate(rule, value);
|
const rules = Joi.alternatives(ruleNull, rule);
|
||||||
|
|
||||||
|
return RuleValidator.validate<NullOr<number>>(rules, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static create(
|
public static create(
|
||||||
value: number,
|
props: IPercentageProps = defaultPercentageProps,
|
||||||
options: INullableValueObjectOptions = {},
|
options: IPercentageOptions = {}
|
||||||
) {
|
) {
|
||||||
|
if (props === null) {
|
||||||
|
throw new Error(`InvalidParams: props params is missing`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { amount = defaultPercentageProps.amount, precision = defaultPercentageProps.precision } =
|
||||||
|
props;
|
||||||
|
|
||||||
const _options = {
|
const _options = {
|
||||||
label: "percentage",
|
label: "percentage",
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
const validationResult = Percentage.validate(value, _options);
|
const validationResult = Percentage.validate(amount, _options);
|
||||||
|
|
||||||
if (validationResult.isFailure) {
|
if (validationResult.isFailure) {
|
||||||
return Result.fail(validationResult.error);
|
return Result.fail(validationResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.ok<Percentage>(new Percentage(value));
|
let _amount: NullOr<number> = Percentage.sanitize(validationResult.object);
|
||||||
|
|
||||||
|
const _props = {
|
||||||
|
amount: isNull(_amount) ? 0 : _amount,
|
||||||
|
precision,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Result.ok<Percentage>(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(percentage: IPercentage, isNull: boolean, options: IPercentageOptions) {
|
||||||
|
super(percentage);
|
||||||
|
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 getAmount(): NullOr<number> {
|
||||||
|
return this.isNull() ? null : Number(this.props?.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPrecision(): number {
|
||||||
|
return this.isNull() ? 0 : Number(this.props?.precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEmpty = (): boolean => {
|
||||||
|
return this.isNull();
|
||||||
|
};
|
||||||
|
|
||||||
|
public isNull = (): boolean => {
|
||||||
|
return this._isNull;
|
||||||
|
};
|
||||||
|
|
||||||
public toNumber(): number {
|
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 {
|
public toString(): string {
|
||||||
return this.isNull() ? "" : String(this.value);
|
return this.isNull() ? "" : String(this.toNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
public toPrimitive(): number {
|
public toPrimitive(): number {
|
||||||
return this.toNumber();
|
return this.toNumber();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export class InvalidPercentageError extends Error {}
|
public toPrimitives() {
|
||||||
|
return this.toObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toObject(): PercentageObject {
|
||||||
|
return {
|
||||||
|
amount: this.amount ? this.amount : 0,
|
||||||
|
precision: this.precision,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasSamePrecision(quantity: Percentage) {
|
||||||
|
return this.precision === quantity.precision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -17,17 +17,26 @@ interface IQuantity {
|
|||||||
precision: number;
|
precision: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface QuantityObject {
|
||||||
|
amount: number;
|
||||||
|
precision: number;
|
||||||
|
}
|
||||||
|
|
||||||
const defaultQuantityProps = {
|
const defaultQuantityProps = {
|
||||||
amount: 1,
|
amount: 0,
|
||||||
precision: 0,
|
precision: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Quantity extends NullableValueObject<IQuantity> {
|
export class Quantity extends NullableValueObject<IQuantity> {
|
||||||
|
public static readonly DEFAULT_PRECISION = defaultQuantityProps.precision;
|
||||||
|
|
||||||
private readonly _isNull: boolean;
|
private readonly _isNull: boolean;
|
||||||
private readonly _options: IQuantityOptions;
|
private readonly _options: IQuantityOptions;
|
||||||
|
|
||||||
protected static validate(value: NullOr<number | string>, options: IQuantityOptions = {}) {
|
protected static validate(value: NullOr<number | string>, options: IQuantityOptions = {}) {
|
||||||
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(null);
|
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(
|
||||||
|
defaultQuantityProps.amount
|
||||||
|
);
|
||||||
|
|
||||||
const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label(
|
const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label(
|
||||||
options.label ? options.label : "quantity"
|
options.label ? options.label : "quantity"
|
||||||
@ -100,6 +109,14 @@ export class Quantity extends NullableValueObject<IQuantity> {
|
|||||||
return this.isNull() ? 0 : Number(this.props?.precision);
|
return this.isNull() ? 0 : Number(this.props?.precision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAmount(): NullOr<number> {
|
||||||
|
return this.isNull() ? null : Number(this.props?.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPrecision(): number {
|
||||||
|
return this.isNull() ? 0 : Number(this.props?.precision);
|
||||||
|
}
|
||||||
|
|
||||||
public isEmpty = (): boolean => {
|
public isEmpty = (): boolean => {
|
||||||
return this.isNull();
|
return this.isNull();
|
||||||
};
|
};
|
||||||
@ -130,9 +147,9 @@ export class Quantity extends NullableValueObject<IQuantity> {
|
|||||||
return this.toObject();
|
return this.toObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
public toObject(): IQuantityProps {
|
public toObject(): QuantityObject {
|
||||||
return {
|
return {
|
||||||
amount: this.amount,
|
amount: this.amount ? this.amount : 0,
|
||||||
precision: this.precision,
|
precision: this.precision,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
export * from "./Address";
|
export * from "./Address";
|
||||||
export * from "./AggregateRoot";
|
export * from "./AggregateRoot";
|
||||||
export * from "./Collection";
|
export * from "./Collection";
|
||||||
export * from "./Currency";
|
export * from "./CurrencyData";
|
||||||
export * from "./Description";
|
export * from "./Description";
|
||||||
export * from "./Email";
|
export * from "./Email";
|
||||||
export * from "./Entity";
|
export * from "./Entity";
|
||||||
@ -19,11 +19,11 @@ export * from "./Result";
|
|||||||
export * from "./ResultCollection";
|
export * from "./ResultCollection";
|
||||||
export * from "./Slug";
|
export * from "./Slug";
|
||||||
export * from "./StringValueObject";
|
export * from "./StringValueObject";
|
||||||
export * from "./TINNumber";
|
|
||||||
export * from "./TextValueObject";
|
export * from "./TextValueObject";
|
||||||
export * from "./UTCDateValue";
|
export * from "./TINNumber";
|
||||||
export * from "./UniqueID";
|
export * from "./UniqueID";
|
||||||
export * from "./UnitPrice";
|
//export * from "./UnitPrice";
|
||||||
|
export * from "./UTCDateValue";
|
||||||
export * from "./ValueObject";
|
export * from "./ValueObject";
|
||||||
|
|
||||||
export * from "./QueryCriteria";
|
export * from "./QueryCriteria";
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import { IMoney_Request_DTO, Result, RuleValidator } from "../../../../../common";
|
import {
|
||||||
|
IMoney_Response_DTO,
|
||||||
|
IPercentage_Response_DTO,
|
||||||
|
IQuantity_Response_DTO,
|
||||||
|
Result,
|
||||||
|
RuleValidator,
|
||||||
|
} from "../../../../../common";
|
||||||
|
|
||||||
export interface ICreateQuote_Request_DTO {
|
export interface ICreateQuote_Request_DTO {
|
||||||
id: string;
|
id: string;
|
||||||
@ -13,14 +19,23 @@ export interface ICreateQuote_Request_DTO {
|
|||||||
notes: string;
|
notes: string;
|
||||||
validity: string;
|
validity: string;
|
||||||
|
|
||||||
|
subtotal: IMoney_Response_DTO;
|
||||||
|
discount: IPercentage_Response_DTO;
|
||||||
|
total: IMoney_Response_DTO;
|
||||||
|
|
||||||
items: ICreateQuoteItem_Request_DTO[];
|
items: ICreateQuoteItem_Request_DTO[];
|
||||||
|
|
||||||
|
dealer_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreateQuoteItem_Request_DTO {
|
export interface ICreateQuoteItem_Request_DTO {
|
||||||
|
article_id: string;
|
||||||
|
quantity: IQuantity_Response_DTO;
|
||||||
description: string;
|
description: string;
|
||||||
quantity: string;
|
unit_price: IMoney_Response_DTO;
|
||||||
unit_measure: string;
|
price: IMoney_Response_DTO;
|
||||||
unit_price: IMoney_Request_DTO;
|
discount: IPercentage_Response_DTO;
|
||||||
|
total_price: IMoney_Response_DTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureCreateQuote_Request_DTOIsValid(quoteDTO: ICreateQuote_Request_DTO) {
|
export function ensureCreateQuote_Request_DTOIsValid(quoteDTO: ICreateQuote_Request_DTO) {
|
||||||
@ -37,14 +52,21 @@ export function ensureCreateQuote_Request_DTOIsValid(quoteDTO: ICreateQuote_Requ
|
|||||||
|
|
||||||
items: Joi.array().items(
|
items: Joi.array().items(
|
||||||
Joi.object({
|
Joi.object({
|
||||||
|
article_id: Joi.string(),
|
||||||
description: Joi.string(),
|
description: Joi.string(),
|
||||||
quantity: Joi.string(),
|
quantity: {
|
||||||
unit_measure: Joi.string(),
|
amount: Joi.number(),
|
||||||
|
precision: Joi.number(),
|
||||||
|
},
|
||||||
unit_price: Joi.object({
|
unit_price: Joi.object({
|
||||||
amount: Joi.number(),
|
amount: Joi.number(),
|
||||||
precision: Joi.number(),
|
precision: Joi.number(),
|
||||||
currency: Joi.string(),
|
currency: Joi.string(),
|
||||||
}),
|
}),
|
||||||
|
discount: Joi.object({
|
||||||
|
amount: Joi.number(),
|
||||||
|
precision: Joi.number(),
|
||||||
|
}),
|
||||||
}).unknown(true)
|
}).unknown(true)
|
||||||
),
|
),
|
||||||
}).unknown(true);
|
}).unknown(true);
|
||||||
|
|||||||
@ -1,23 +1,31 @@
|
|||||||
import { IMoney_Response_DTO } from "../../../../../common";
|
import {
|
||||||
|
IMoney_Response_DTO,
|
||||||
|
IPercentage_Response_DTO,
|
||||||
|
IQuantity_Response_DTO,
|
||||||
|
} from "../../../../../common";
|
||||||
|
|
||||||
export interface ICreateQuote_Response_DTO {
|
export interface ICreateQuote_Response_DTO {
|
||||||
id: string;
|
id: string;
|
||||||
status: string;
|
status: string;
|
||||||
date: string;
|
date: string;
|
||||||
|
reference: string;
|
||||||
|
customer_information: string;
|
||||||
lang_code: string;
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
payment_method: string;
|
||||||
|
notes: string;
|
||||||
|
validity: string;
|
||||||
|
|
||||||
subtotal: IMoney_Response_DTO;
|
discount: IPercentage_Response_DTO;
|
||||||
total: IMoney_Response_DTO;
|
|
||||||
|
|
||||||
items: ICreateQuote_QuoteItem_Response_DTO[];
|
items: ICreateQuote_QuoteItem_Response_DTO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreateQuote_QuoteItem_Response_DTO {
|
export interface ICreateQuote_QuoteItem_Response_DTO {
|
||||||
|
article_id: string;
|
||||||
|
quantity: IQuantity_Response_DTO;
|
||||||
description: string;
|
description: string;
|
||||||
quantity: string;
|
|
||||||
unit_measure: string;
|
|
||||||
unit_price: IMoney_Response_DTO;
|
unit_price: IMoney_Response_DTO;
|
||||||
subtotal: IMoney_Response_DTO;
|
price: IMoney_Response_DTO;
|
||||||
total: IMoney_Response_DTO;
|
discount: IPercentage_Response_DTO;
|
||||||
|
total_price: IMoney_Response_DTO;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
import { IMoney_Response_DTO } from "../../../../../common";
|
import {
|
||||||
|
IMoney_Response_DTO,
|
||||||
|
IPercentage_Response_DTO,
|
||||||
|
IQuantity_Response_DTO,
|
||||||
|
} from "../../../../../common";
|
||||||
|
|
||||||
export interface IGetQuote_Response_DTO {
|
export interface IGetQuote_Response_DTO {
|
||||||
id: string;
|
id: string;
|
||||||
@ -8,21 +12,26 @@ export interface IGetQuote_Response_DTO {
|
|||||||
customer_information: string;
|
customer_information: string;
|
||||||
lang_code: string;
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
|
||||||
payment_method: string;
|
payment_method: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
validity: string;
|
validity: string;
|
||||||
|
|
||||||
subtotal: IMoney_Response_DTO;
|
subtotal_price: IMoney_Response_DTO;
|
||||||
total: IMoney_Response_DTO;
|
discount: IPercentage_Response_DTO;
|
||||||
|
total_price: IMoney_Response_DTO;
|
||||||
|
|
||||||
items: IGetQuote_QuoteItem_Response_DTO[];
|
items: IGetQuote_QuoteItem_Response_DTO[];
|
||||||
|
|
||||||
|
dealer_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGetQuote_QuoteItem_Response_DTO {
|
export interface IGetQuote_QuoteItem_Response_DTO {
|
||||||
|
article_id: string;
|
||||||
|
quantity: IQuantity_Response_DTO;
|
||||||
description: string;
|
description: string;
|
||||||
quantity: string;
|
|
||||||
unit_measure: string;
|
|
||||||
unit_price: IMoney_Response_DTO;
|
unit_price: IMoney_Response_DTO;
|
||||||
subtotal: IMoney_Response_DTO;
|
subtotal_price: IMoney_Response_DTO;
|
||||||
total: IMoney_Response_DTO;
|
discount: IPercentage_Response_DTO;
|
||||||
|
total_price: IMoney_Response_DTO;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { IMoney_Response_DTO } from "../../../../../common";
|
import { IQuantuty_Response_DTO } from "../../../../../common";
|
||||||
|
|
||||||
export interface IListQuotes_Response_DTO {
|
export interface IListQuotes_Response_DTO {
|
||||||
id: string;
|
id: string;
|
||||||
@ -9,6 +9,6 @@ export interface IListQuotes_Response_DTO {
|
|||||||
lang_code: string;
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
|
||||||
subtotal: IMoney_Response_DTO;
|
subtotal: IQuantuty_Response_DTO;
|
||||||
total: IMoney_Response_DTO;
|
total: IQuantuty_Response_DTO;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import { IMoney_Request_DTO, Result, RuleValidator } from "../../../../../common";
|
import {
|
||||||
|
IMoney_Request_DTO,
|
||||||
|
IMoney_Response_DTO,
|
||||||
|
IPercentage_Request_DTO,
|
||||||
|
IPercentage_Response_DTO,
|
||||||
|
IQuantity_Response_DTO,
|
||||||
|
Result,
|
||||||
|
RuleValidator,
|
||||||
|
} from "../../../../../common";
|
||||||
|
|
||||||
export interface IUpdateQuote_Request_DTO {
|
export interface IUpdateQuote_Request_DTO {
|
||||||
status: string;
|
status: string;
|
||||||
@ -12,37 +20,71 @@ export interface IUpdateQuote_Request_DTO {
|
|||||||
notes: string;
|
notes: string;
|
||||||
validity: string;
|
validity: string;
|
||||||
|
|
||||||
|
subtotal: IMoney_Request_DTO;
|
||||||
|
discount: IPercentage_Request_DTO;
|
||||||
items: IUpdateQuoteItem_Request_DTO[];
|
items: IUpdateQuoteItem_Request_DTO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUpdateQuoteItem_Request_DTO {
|
export interface IUpdateQuoteItem_Request_DTO {
|
||||||
|
article_id: string;
|
||||||
|
quantity: IQuantity_Response_DTO;
|
||||||
description: string;
|
description: string;
|
||||||
quantity: string;
|
unit_price: IMoney_Response_DTO;
|
||||||
unit_measure: string;
|
subtotal_price: IMoney_Response_DTO;
|
||||||
unit_price: IMoney_Request_DTO;
|
discount: IPercentage_Response_DTO;
|
||||||
|
total_price: IMoney_Response_DTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureUpdateQuote_Request_DTOIsValid(quoteDTO: IUpdateQuote_Request_DTO) {
|
export function ensureUpdateQuote_Request_DTOIsValid(quoteDTO: IUpdateQuote_Request_DTO) {
|
||||||
const schema = Joi.object({
|
const schema = Joi.object({
|
||||||
|
status: Joi.string(),
|
||||||
date: Joi.string(),
|
date: Joi.string(),
|
||||||
reference: Joi.string(),
|
reference: Joi.string(),
|
||||||
lang_code: Joi.string(),
|
|
||||||
customer_information: Joi.string(),
|
customer_information: Joi.string(),
|
||||||
|
lang_code: Joi.string(),
|
||||||
currency_code: Joi.string(),
|
currency_code: Joi.string(),
|
||||||
payment_method: Joi.string(),
|
payment_method: Joi.string(),
|
||||||
notes: Joi.string(),
|
notes: Joi.string(),
|
||||||
validity: Joi.string(),
|
validity: Joi.string(),
|
||||||
|
|
||||||
|
subtotal: Joi.object({
|
||||||
|
amount: Joi.number(),
|
||||||
|
precision: Joi.number(),
|
||||||
|
currency: Joi.string(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
discount: Joi.object({
|
||||||
|
amount: Joi.number(),
|
||||||
|
precision: Joi.number(),
|
||||||
|
}),
|
||||||
|
|
||||||
items: Joi.array().items(
|
items: Joi.array().items(
|
||||||
Joi.object({
|
Joi.object({
|
||||||
|
article_id: Joi.string(),
|
||||||
|
quantity: Joi.object({
|
||||||
|
amount: Joi.number(),
|
||||||
|
precision: Joi.number(),
|
||||||
|
}),
|
||||||
description: Joi.string(),
|
description: Joi.string(),
|
||||||
quantity: Joi.string(),
|
|
||||||
unit_measure: Joi.string(),
|
|
||||||
unit_price: Joi.object({
|
unit_price: Joi.object({
|
||||||
amount: Joi.number(),
|
amount: Joi.number(),
|
||||||
precision: Joi.number(),
|
precision: Joi.number(),
|
||||||
currency: Joi.string(),
|
currency: Joi.string(),
|
||||||
}),
|
}),
|
||||||
|
subtotal_price: Joi.object({
|
||||||
|
amount: Joi.number(),
|
||||||
|
precision: Joi.number(),
|
||||||
|
currency: Joi.string(),
|
||||||
|
}),
|
||||||
|
discount: Joi.object({
|
||||||
|
amount: Joi.number(),
|
||||||
|
precision: Joi.number(),
|
||||||
|
}),
|
||||||
|
total_price: Joi.object({
|
||||||
|
amount: Joi.number(),
|
||||||
|
precision: Joi.number(),
|
||||||
|
currency: Joi.string(),
|
||||||
|
}),
|
||||||
}).unknown(true)
|
}).unknown(true)
|
||||||
),
|
),
|
||||||
}).unknown(true);
|
}).unknown(true);
|
||||||
|
|||||||
@ -1,23 +1,33 @@
|
|||||||
import { IMoney_Response_DTO } from "shared/lib/contexts/common";
|
import { IMoney_Response_DTO } from "shared/lib/contexts/common";
|
||||||
|
import { IPercentage_Response_DTO, IQuantity_Response_DTO } from "../../../../../common";
|
||||||
|
|
||||||
export interface IUpdateQuote_Response_DTO {
|
export interface IUpdateQuote_Response_DTO {
|
||||||
id: string;
|
id: string;
|
||||||
status: string;
|
status: string;
|
||||||
date: string;
|
date: string;
|
||||||
language_code: string;
|
reference: string;
|
||||||
|
customer_information: string;
|
||||||
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
payment_method: string;
|
||||||
|
notes: string;
|
||||||
|
validity: string;
|
||||||
|
|
||||||
subtotal: IMoney_Response_DTO;
|
subtotal: IMoney_Response_DTO;
|
||||||
|
discount: IPercentage_Response_DTO;
|
||||||
total: IMoney_Response_DTO;
|
total: IMoney_Response_DTO;
|
||||||
|
|
||||||
items: IUpdateQuote_QuoteItem_Response_DTO[];
|
items: IUpdateQuote_QuoteItem_Response_DTO[];
|
||||||
|
|
||||||
|
dealer_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUpdateQuote_QuoteItem_Response_DTO {
|
export interface IUpdateQuote_QuoteItem_Response_DTO {
|
||||||
|
article_id: string;
|
||||||
|
quantity: IQuantity_Response_DTO;
|
||||||
description: string;
|
description: string;
|
||||||
quantity: string;
|
|
||||||
unit_measure: string;
|
|
||||||
unit_price: IMoney_Response_DTO;
|
unit_price: IMoney_Response_DTO;
|
||||||
subtotal: IMoney_Response_DTO;
|
price: IMoney_Response_DTO;
|
||||||
total: IMoney_Response_DTO;
|
discount: IPercentage_Response_DTO;
|
||||||
|
total_price: IMoney_Response_DTO;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user