.
This commit is contained in:
parent
ae0dabfca3
commit
981d70cffe
@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
$schema: "https://json.schemastore.org/eslintrc",
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
@ -12,5 +13,6 @@ module.exports = {
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"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 { Routes } from "./Routes";
|
||||
import { LoadingOverlay, TailwindIndicator } from "./components";
|
||||
import { createAxiosDataProvider } from "./lib/axios";
|
||||
import { createAxiosAuthActions } from "./lib/axios/createAxiosAuthActions";
|
||||
import { DataSourceProvider } from "./lib/hooks/useDataSource/DataSourceContext";
|
||||
import { createAxiosAuthActions, createAxiosDataProvider } from "./lib/axios";
|
||||
import { DataSourceProvider } from "./lib/hooks";
|
||||
|
||||
function App() {
|
||||
const queryClient = new QueryClient({
|
||||
@ -28,6 +27,7 @@ function App() {
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Suspense fallback={<LoadingOverlay />}>
|
||||
<Routes />
|
||||
|
||||
<ToastContainer />
|
||||
</Suspense>
|
||||
</TooltipProvider>
|
||||
|
||||
@ -86,7 +86,7 @@ export const Routes = () => {
|
||||
element: <QuoteCreate />,
|
||||
},
|
||||
{
|
||||
path: "edit",
|
||||
path: "edit/:id",
|
||||
element: <QuoteEdit />,
|
||||
},
|
||||
],
|
||||
@ -109,11 +109,7 @@ export const Routes = () => {
|
||||
},
|
||||
{
|
||||
path: "/logout",
|
||||
element: (
|
||||
<ProtectedRoute>
|
||||
<LogoutPage />
|
||||
</ProtectedRoute>
|
||||
),
|
||||
element: <LogoutPage />,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
const columns = useMemo<ColumnDef<IListArticles_Response_DTO, any>[]>(
|
||||
const columns = useMemo<ColumnDef<IListArticles_Response_DTO>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "id" as const,
|
||||
@ -65,7 +65,7 @@ export const CatalogDataTable = () => {
|
||||
id: "retail_price" as const,
|
||||
accessorKey: "retail_price",
|
||||
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;
|
||||
return <div className='text-right'>{price.toFormat()}</div>;
|
||||
},
|
||||
@ -113,10 +113,8 @@ export const CatalogDataTable = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable table={table} paginationOptions={{ visible: true }}>
|
||||
<DataTableToolbar table={table} />
|
||||
</DataTable>
|
||||
</>
|
||||
<DataTable table={table} paginationOptions={{ visible: true }} title='Catálogo'>
|
||||
<DataTableToolbar table={table} />
|
||||
</DataTable>
|
||||
);
|
||||
};
|
||||
|
||||
@ -32,6 +32,7 @@ import {
|
||||
PaginationItem,
|
||||
Progress,
|
||||
Separator,
|
||||
Skeleton,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@ -44,20 +45,24 @@ import {
|
||||
TabsTrigger,
|
||||
} from "@/ui";
|
||||
import { t } from "i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const DashboardPage = () => {
|
||||
const { data, status } = useGetIdentity();
|
||||
const navigate = useNavigate();
|
||||
const { data: userIdentity, status } = useGetIdentity();
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<LayoutHeader />
|
||||
<LayoutContent>
|
||||
{status === "success" && (
|
||||
{status === "success" ? (
|
||||
<div className='flex items-center'>
|
||||
<h1 className='text-lg font-semibold md:text-2xl'>{`${t("dashboard.welcome")}, ${
|
||||
data?.name
|
||||
userIdentity?.name
|
||||
}`}</h1>
|
||||
</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'>
|
||||
@ -72,7 +77,9 @@ export const DashboardPage = () => {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter>
|
||||
<Button>Crear nueva cotización</Button>
|
||||
<Button onClick={() => navigate("/quotes/add")}>
|
||||
{t("quotes.create.title")}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<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 [activeId, setActiveId] = useState<UniqueIdentifier | null>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]);
|
||||
|
||||
@ -145,8 +146,8 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: (originalRow: unknown) => originalRow?.id,
|
||||
debugHeaders: true,
|
||||
debugColumns: true,
|
||||
debugHeaders: false,
|
||||
debugColumns: false,
|
||||
meta: {
|
||||
insertItem: (rowIndex: number, data: object = {}) => {
|
||||
actions.insert(rowIndex, data, { shouldFocus: true });
|
||||
@ -263,16 +264,43 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
|
||||
const hadleNewItem = useCallback(() => {
|
||||
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]);
|
||||
|
||||
@ -1,20 +1,21 @@
|
||||
import {
|
||||
ButtonGroup,
|
||||
CancelButton,
|
||||
FormGroup,
|
||||
FormMoneyField,
|
||||
FormPercentageField,
|
||||
FormQuantityField,
|
||||
FormTextAreaField,
|
||||
FormTextField,
|
||||
SubmitButton,
|
||||
} from "@/components";
|
||||
import { Input } from "@/ui";
|
||||
import { t } from "i18next";
|
||||
import { DataTableProvider } from "@/lib/hooks";
|
||||
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 { useDetailColumns } from "../../hooks";
|
||||
import { CatalogPickerDataTable } from "../CatalogPickerDataTable";
|
||||
import { SortableDataTable } from "../SortableDataTable";
|
||||
|
||||
export const QuoteDetailsCardEditor = () => {
|
||||
const { control, register, formState } = useFormContext();
|
||||
const { control, register, watch, getValues, setValue } = useFormContext();
|
||||
|
||||
const { fields, ...fieldActions } = useFieldArray({
|
||||
control,
|
||||
@ -39,27 +40,16 @@ export const QuoteDetailsCardEditor = () => {
|
||||
accessorKey: "quantity",
|
||||
header: "quantity",
|
||||
size: 5,
|
||||
cell: ({ row: { index }, column: { id } }) => {
|
||||
return (
|
||||
<FormTextField
|
||||
type='number'
|
||||
control={control}
|
||||
{...register(`items.${index}.quantity`)}
|
||||
/>
|
||||
);
|
||||
cell: ({ row: { index } }) => {
|
||||
return <FormQuantityField {...register(`items.${index}.quantity`)} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "description" as const,
|
||||
accessorKey: "description",
|
||||
cell: ({ row: { index }, column: { id } }) => {
|
||||
return (
|
||||
<FormTextAreaField
|
||||
autoSize
|
||||
control={control}
|
||||
{...register(`items.${index}.description`)}
|
||||
/>
|
||||
);
|
||||
header: "description",
|
||||
cell: ({ row: { index } }) => {
|
||||
return <FormTextAreaField autoSize {...register(`items.${index}.description`)} />;
|
||||
},
|
||||
},
|
||||
|
||||
@ -69,7 +59,7 @@ export const QuoteDetailsCardEditor = () => {
|
||||
header: "retail_price",
|
||||
size: 10,
|
||||
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",
|
||||
size: 10,
|
||||
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",
|
||||
size: 5,
|
||||
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",
|
||||
size: 10,
|
||||
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 (
|
||||
<FormGroup
|
||||
title={t("quotes.create.tabs.items.title")}
|
||||
description={t("quotes.create.tabs.items.desc")}
|
||||
actions={
|
||||
<ButtonGroup className='md:hidden'>
|
||||
<CancelButton onClick={() => null} size='sm' />
|
||||
<SubmitButton
|
||||
disabled={!formState.isDirty || formState.isSubmitting || formState.isLoading}
|
||||
label='Guardar'
|
||||
size='sm'
|
||||
/>
|
||||
</ButtonGroup>
|
||||
}
|
||||
<ResizablePanelGroup
|
||||
direction='horizontal'
|
||||
autoSaveId='uecko.quotes.details_layout'
|
||||
className='items-stretch h-full'
|
||||
>
|
||||
<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} />
|
||||
</div>
|
||||
</FormGroup>
|
||||
</ResizablePanel>
|
||||
<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 { SubmitButton } from "@/components";
|
||||
import { useWarnAboutChange } from "@/lib/hooks";
|
||||
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 { useNavigate } from "react-router-dom";
|
||||
import { useQuotes } from "./hooks";
|
||||
|
||||
type QuoteDataForm = {
|
||||
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;
|
||||
};*/
|
||||
interface QuoteDataForm extends ICreateQuote_Request_DTO {}
|
||||
|
||||
export const QuoteCreate = () => {
|
||||
//const [loading, setLoading] = useState(false);
|
||||
|
||||
//const { data: userIdentity } = useGetIdentity();
|
||||
//console.log(userIdentity);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { useMutation } = useQuotes();
|
||||
const { mutate } = useMutation();
|
||||
const { setWarnWhen } = useWarnAboutChange();
|
||||
const { useCreate } = useQuotes();
|
||||
const { mutate } = useCreate();
|
||||
|
||||
const form = useForm<QuoteDataForm>({
|
||||
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) => {
|
||||
alert(JSON.stringify(formData));
|
||||
console.log(JSON.stringify(formData));
|
||||
|
||||
try {
|
||||
//setLoading(true);
|
||||
setWarnWhen(false);
|
||||
mutate(formData, {
|
||||
onSuccess: (data) => {
|
||||
navigate(`/quotes/edit/${data.id}`, { relative: "path", replace: true });
|
||||
@ -73,10 +68,15 @@ export const QuoteCreate = () => {
|
||||
|
||||
return (
|
||||
<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='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' />
|
||||
<span className='sr-only'>{t("quotes.common.back")}</span>
|
||||
</Button>
|
||||
@ -85,7 +85,7 @@ export const QuoteCreate = () => {
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className='grid max-w-lg gap-6'>
|
||||
<div className='grid w-6/12 gap-6 mx-auto'>
|
||||
<FormTextField
|
||||
className='row-span-2'
|
||||
name='reference'
|
||||
@ -112,12 +112,17 @@ export const QuoteCreate = () => {
|
||||
description={t("quotes.create.form_fields.customer_information.desc")}
|
||||
placeholder={t("quotes.create.form_fields.customer_information.placeholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-start gap-2'>
|
||||
<BackHistoryButton size='sm' label={t("quotes.create.buttons.discard")} url='/quotes' />
|
||||
<div className='flex items-center justify-around gap-2'>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
|
||||
import { SubmitButton } from "@/components";
|
||||
import { FormMoneyField, LoadingOverlay, SubmitButton } from "@/components";
|
||||
import { calculateItemTotals } from "@/lib/calc";
|
||||
import { useGetIdentity } from "@/lib/hooks";
|
||||
import { useUrlId } from "@/lib/hooks/useUrlId";
|
||||
import { Badge, Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui";
|
||||
import { IUpdateQuote_Request_DTO, MoneyValue } from "@shared/contexts";
|
||||
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 {
|
||||
QuoteDetailsCardEditor,
|
||||
QuoteDocumentsCardEditor,
|
||||
QuoteGeneralCardEditor,
|
||||
} from "./components/editors";
|
||||
import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors";
|
||||
import { useQuotes } from "./hooks";
|
||||
|
||||
type QuoteDataForm = {
|
||||
id: string;
|
||||
status: string;
|
||||
// simple typesafe helperfunction
|
||||
type EndsWith<T, b extends string> = T extends `${infer f}${b}` ? T : never;
|
||||
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;
|
||||
reference: string;
|
||||
customer_information: string;
|
||||
@ -24,23 +26,31 @@ type QuoteDataForm = {
|
||||
payment_method: string;
|
||||
notes: string;
|
||||
validity: string;
|
||||
items: any[];
|
||||
};
|
||||
discount: IPercentage;
|
||||
|
||||
type QuoteCreateProps = {
|
||||
isOverModal?: boolean;
|
||||
};
|
||||
subtotal: IMoney;
|
||||
|
||||
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 quoteId = useUrlId();
|
||||
|
||||
const { data: userIdentity } = useGetIdentity();
|
||||
console.log(userIdentity);
|
||||
|
||||
const { useQuery, useMutation } = useQuotes();
|
||||
const { useOne, useUpdate } = useQuotes();
|
||||
|
||||
const { data } = useQuery;
|
||||
const { mutate } = useMutation;
|
||||
const { data, status } = useOne(quoteId);
|
||||
const { mutate } = useUpdate(quoteId);
|
||||
|
||||
const form = useForm<QuoteDataForm>({
|
||||
mode: "onBlur",
|
||||
@ -54,53 +64,119 @@ export const QuoteEdit = ({ isOverModal }: QuoteCreateProps) => {
|
||||
payment_method: "",
|
||||
notes: "",
|
||||
validity: "",
|
||||
subtotal: "",
|
||||
items: [],
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<QuoteDataForm> = async (data) => {
|
||||
alert(JSON.stringify(data));
|
||||
console.debug(JSON.stringify(data));
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
data.currency_code = "EUR";
|
||||
data.lang_code = String(userIdentity?.language);
|
||||
|
||||
mutate(data);
|
||||
// Transformación del form -> typo de request
|
||||
mutate(data, {
|
||||
onError: (error) => {
|
||||
alert(error);
|
||||
},
|
||||
//onSettled: () => {},
|
||||
onSuccess: () => {
|
||||
alert("guardado");
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
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 (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className='mx-auto grid max-w-[90rem] flex-1 auto-rows-max gap-6'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<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>
|
||||
</Button>
|
||||
<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>
|
||||
<Badge variant='default' className='ml-auto sm:ml-0'>
|
||||
{t("quotes.status.draft")}
|
||||
{data.status}
|
||||
</Badge>
|
||||
<div className='items-center hidden gap-2 md:ml-auto md:flex'>
|
||||
<Button variant='outline' size='sm'>
|
||||
{t("quotes.create.buttons.discard")}
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<SubmitButton variant={form.formState.isDirty ? "default" : "outline"} size='sm'>
|
||||
{t("quotes.create.buttons.save_quote")}
|
||||
{t("common.save")}
|
||||
</SubmitButton>
|
||||
</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>
|
||||
<TabsTrigger value='general'>{t("quotes.create.tabs.general")}</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>
|
||||
</TabsList>
|
||||
<TabsContent value='general'>
|
||||
@ -110,9 +186,6 @@ export const QuoteEdit = ({ isOverModal }: QuoteCreateProps) => {
|
||||
<QuoteDetailsCardEditor />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='documents'>
|
||||
<QuoteDocumentsCardEditor />
|
||||
</TabsContent>
|
||||
<TabsContent value='history'></TabsContent>
|
||||
</Tabs>
|
||||
<div className='flex items-center justify-center gap-2 md:hidden'>
|
||||
|
||||
@ -6,6 +6,8 @@ import {
|
||||
ICreateQuote_Request_DTO,
|
||||
ICreateQuote_Response_DTO,
|
||||
IGetQuote_Response_DTO,
|
||||
IUpdateQuote_Request_DTO,
|
||||
IUpdateQuote_Response_DTO,
|
||||
UniqueID,
|
||||
} from "@shared/contexts";
|
||||
|
||||
@ -14,22 +16,23 @@ export type UseQuotesGetParamsType = {
|
||||
queryOptions?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const useQuotes = (params?: UseQuotesGetParamsType) => {
|
||||
export const useQuotes = () => {
|
||||
const dataSource = useDataSource();
|
||||
const keys = useQueryKey();
|
||||
|
||||
return {
|
||||
useQuery: () =>
|
||||
useOne: (id?: string, params?: UseQuotesGetParamsType) =>
|
||||
useOne<IGetQuote_Response_DTO>({
|
||||
queryKey: keys().data().resource("quotes").action("one").id("").params().get(),
|
||||
queryFn: () =>
|
||||
dataSource.getOne({
|
||||
resource: "quotes",
|
||||
id: "",
|
||||
id: String(id),
|
||||
}),
|
||||
enabled: !!id,
|
||||
...params,
|
||||
}),
|
||||
useMutation: () =>
|
||||
useCreate: () =>
|
||||
useSave<ICreateQuote_Response_DTO, TDataSourceError, ICreateQuote_Request_DTO>({
|
||||
mutationKey: keys().data().resource("quotes").action("one").id("").params().get(),
|
||||
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 { PropsWithChildren, ReactNode } from "react";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Separator,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
@ -8,11 +16,9 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/ui/table";
|
||||
import { PropsWithChildren, ReactNode } from "react";
|
||||
} from "@/ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader } from "@/ui";
|
||||
import { DataTableColumnHeader } from "./DataTableColumnHeader";
|
||||
import { DataTablePagination, DataTablePaginationProps } from "./DataTablePagination";
|
||||
|
||||
@ -23,38 +29,58 @@ export type DataTablePaginationOptionsProps<TData> = Pick<
|
||||
"visible"
|
||||
>;
|
||||
|
||||
export type DataTableHeaderOptionsProps = {
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
export type DataTableProps<TData> = PropsWithChildren<{
|
||||
table: ReactTable<TData>;
|
||||
title?: ReactNode;
|
||||
description?: ReactNode;
|
||||
caption?: ReactNode;
|
||||
paginationOptions?: DataTablePaginationOptionsProps<TData>;
|
||||
headerOptions?: DataTableHeaderOptionsProps;
|
||||
className?: string;
|
||||
rowClassName?: string;
|
||||
cellClassName?: string;
|
||||
}>;
|
||||
|
||||
export function DataTable<TData>({
|
||||
table,
|
||||
title,
|
||||
description,
|
||||
caption,
|
||||
paginationOptions,
|
||||
headerOptions = { visible: true },
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
rowClassName,
|
||||
cellClassName,
|
||||
}: DataTableProps<TData>) {
|
||||
const headerVisible = headerOptions?.visible;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<Card className={className}>
|
||||
{(title || description) && (
|
||||
<CardHeader className='pb-0'>
|
||||
<CardDescription
|
||||
className={cn("w-full space-y-2.5 overflow-auto mt-7", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</CardDescription>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='pt-6'>
|
||||
<Table>
|
||||
{typeof caption !== "undefined" && <TableCaption>{caption}</TableCaption>}
|
||||
)}
|
||||
<CardContent className='pt-6'>
|
||||
{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>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
<TableRow key={headerGroup.id} className={rowClassName}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead
|
||||
@ -69,35 +95,42 @@ export function DataTable<TData>({
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={table.getAllColumns.length} className='h-24 text-center'>
|
||||
No hay datos para mostrar
|
||||
</TableCell>
|
||||
)}
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className={rowClassName}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className={cellClassName}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<DataTablePagination
|
||||
className='flex-1'
|
||||
visible={paginationOptions?.visible}
|
||||
table={table}
|
||||
/>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</>
|
||||
))
|
||||
) : (
|
||||
<TableRow className={rowClassName}>
|
||||
<TableCell
|
||||
className={cn("h-24 text-center", cellClassName)}
|
||||
colSpan={table.getAllColumns.length}
|
||||
>
|
||||
No hay datos para mostrar
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</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
|
||||
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
|
||||
)}
|
||||
>
|
||||
|
||||
@ -68,6 +68,7 @@ export function DataTablePagination<TData>({
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='hidden w-8 h-8 p-0 lg:flex'
|
||||
onClick={() => table.setPageIndex(INITIAL_PAGE_INDEX)}
|
||||
@ -77,6 +78,7 @@ export function DataTablePagination<TData>({
|
||||
<ChevronsLeftIcon className='w-4 h-4' />
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='w-8 h-8 p-0'
|
||||
onClick={() => table.previousPage()}
|
||||
@ -86,6 +88,7 @@ export function DataTablePagination<TData>({
|
||||
<ChevronLeftIcon className='w-4 h-4' />
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='w-8 h-8 p-0'
|
||||
onClick={() => table.nextPage()}
|
||||
@ -95,6 +98,7 @@ export function DataTablePagination<TData>({
|
||||
<ChevronRightIcon className='w-4 h-4' />
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='hidden w-8 h-8 p-0 lg:flex'
|
||||
onClick={() => table.setPageIndex(table.getPageCount() + 1)}
|
||||
|
||||
@ -1,24 +1,21 @@
|
||||
import { Checkbox } from "@/ui";
|
||||
import { DataTableColumnProps } from "./DataTable";
|
||||
|
||||
export function getDataTableSelectionColumn<
|
||||
TData,
|
||||
TError,
|
||||
>(): DataTableColumnProps<TData, TError> {
|
||||
export function getDataTableSelectionColumn<TData, TError>(): DataTableColumnProps<TData, TError> {
|
||||
return {
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
id="select-all"
|
||||
id='select-all'
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Seleccionar todo"
|
||||
className="translate-y-[2px]"
|
||||
aria-label='Seleccionar todo'
|
||||
className='translate-y-[2px]'
|
||||
/>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
cell: ({ row, table }) => (
|
||||
<Checkbox
|
||||
id={`select-row-${row.id}`}
|
||||
@ -26,8 +23,8 @@ export function getDataTableSelectionColumn<
|
||||
onCheckedChange={(value) => {
|
||||
row.toggleSelected(!!value);
|
||||
}}
|
||||
aria-label="Seleccionar file"
|
||||
className="translate-y-[2px]"
|
||||
aria-label='Seleccionar file'
|
||||
className='translate-y-[2px]'
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
|
||||
@ -11,10 +11,12 @@ import { DataTableColumnOptions } from "./DataTableColumnOptions";
|
||||
interface DataTableToolbarProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
|
||||
table: Table<TData>;
|
||||
filterFields?: DataTableFilterField<TData>[];
|
||||
fullWidthFilter?: boolean;
|
||||
}
|
||||
|
||||
export function DataTableToolbar<TData>({
|
||||
table,
|
||||
fullWidthFilter,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
@ -35,7 +37,7 @@ export function DataTableToolbar<TData>({
|
||||
placeholder={t("common.filter_placeholder")}
|
||||
value={globalFilter}
|
||||
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 && (
|
||||
|
||||
@ -1,126 +1,136 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from "@/ui";
|
||||
import { FormControl, FormDescription, 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";
|
||||
|
||||
type FormMoneyFieldProps = Omit<FormTextFieldProps, "type">;
|
||||
type FormMoneyFieldProps = Omit<FormTextFieldProps, "type"> & {
|
||||
defaultValue?: any;
|
||||
};
|
||||
|
||||
// Spanish currency config
|
||||
const moneyFormatter = Intl.NumberFormat("es-ES", {
|
||||
currency: "EUR",
|
||||
currencyDisplay: "symbol",
|
||||
currencySign: "standard",
|
||||
style: "currency",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
export function FormMoneyField({
|
||||
label,
|
||||
placeholder,
|
||||
hint,
|
||||
description,
|
||||
required,
|
||||
className,
|
||||
leadIcon,
|
||||
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);
|
||||
}*/
|
||||
export const FormMoneyField = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & FormMoneyFieldProps
|
||||
>((props, ref) => {
|
||||
const {
|
||||
label,
|
||||
placeholder,
|
||||
hint,
|
||||
description,
|
||||
required,
|
||||
className,
|
||||
leadIcon,
|
||||
trailIcon,
|
||||
button,
|
||||
disabled,
|
||||
name,
|
||||
defaultValue,
|
||||
} = props;
|
||||
|
||||
//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 = {
|
||||
input: (value: any) =>
|
||||
isNaN(value) || value === 0 ? "" : moneyFormatter.format(value),
|
||||
input: (value: IMoney) => {
|
||||
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>) => {
|
||||
const output = parseInt(event.target.value, 10);
|
||||
return isNaN(output) ? 0 : output;
|
||||
const output = parseFloat(event.target.value);
|
||||
|
||||
const moneyOrError = MoneyValue.create({
|
||||
amount: output * Math.pow(10, precision),
|
||||
precision,
|
||||
currencyCode,
|
||||
});
|
||||
|
||||
if (moneyOrError.isFailure) {
|
||||
throw moneyOrError.error;
|
||||
}
|
||||
|
||||
return moneyOrError.object.toObject();
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<FormField
|
||||
<Controller
|
||||
defaultValue={defaultValue}
|
||||
control={control}
|
||||
name={name}
|
||||
rules={{ required }}
|
||||
disabled={disabled}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
render={({ field, fieldState, formState }) => {
|
||||
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} />}
|
||||
<div className={cn(button ? "flex" : null)}>
|
||||
<div
|
||||
className={cn(
|
||||
leadIcon
|
||||
? "relative flex items-stretch flex-grow focus-within:z-10"
|
||||
: "",
|
||||
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">
|
||||
<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,
|
||||
null
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormControl
|
||||
className={cn(
|
||||
"block",
|
||||
leadIcon ? "pl-10" : "",
|
||||
trailIcon ? "pr-10" : "",
|
||||
)}
|
||||
className={cn("block", leadIcon ? "pl-10" : "", trailIcon ? "pr-10" : "")}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
fieldState.error ? "border-destructive focus-visible:ring-destructive" : ""
|
||||
)}
|
||||
{...field}
|
||||
onChange={(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">
|
||||
<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,
|
||||
null
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -129,9 +139,9 @@ export function FormMoneyField({
|
||||
</div>
|
||||
{description && <FormDescription>{description}</FormDescription>}
|
||||
<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 "./FormLabel";
|
||||
export * from "./FormMoneyField";
|
||||
export * from "./FormPercentageField";
|
||||
export * from "./FormQuantityField";
|
||||
export * from "./FormTextAreaField";
|
||||
export * from "./FormTextField";
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { UnsavedChangesNotifier, UnsavedWarnProvider } from "@/lib/hooks";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
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";
|
||||
|
||||
@ -10,7 +10,6 @@ export type LoadingIndicatorProps = {
|
||||
subtitle?: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export const LoadingIndicator = ({
|
||||
active = true,
|
||||
look = "dark",
|
||||
|
||||
@ -10,6 +10,13 @@ type ProctectRouteProps = {
|
||||
export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
|
||||
const { isPending, isSuccess, data: { authenticated, redirectTo } = {} } = useIsLoggedIn();
|
||||
|
||||
console.debug("ProtectedRouter", {
|
||||
isPending,
|
||||
isSuccess,
|
||||
authenticated,
|
||||
redirectTo,
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
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()
|
||||
): IAuthActions => ({
|
||||
login: async ({ email, password }: ILogin_DTO) => {
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
const result = await httpClient.request<ILogin_Response_DTO>({
|
||||
url: `${apiUrl}/auth/login`,
|
||||
@ -55,16 +54,14 @@ export const createAxiosAuthActions = (
|
||||
? {
|
||||
authenticated: true,
|
||||
}
|
||||
: { authenticated: false, redirectTo: "/login" }
|
||||
: {
|
||||
authenticated: false,
|
||||
redirectTo: "/login",
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
getIdentity: async () => {
|
||||
const errorResult = {
|
||||
message: "Identification failed",
|
||||
name: "Invalid profile or identification",
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await httpClient.request<IIdentity_Response_DTO>({
|
||||
url: `${apiUrl}/auth/identity`,
|
||||
@ -78,14 +75,13 @@ export const createAxiosAuthActions = (
|
||||
secureLocalStorage.setItem("uecko.profile", data);
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
return Promise.reject(errorResult);
|
||||
return Promise.resolve(null);
|
||||
} catch (error) {
|
||||
return Promise.reject(errorResult);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
},
|
||||
|
||||
onError: (error: any) => {
|
||||
console.error(error);
|
||||
secureLocalStorage.clear();
|
||||
return Promise.resolve({
|
||||
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 "./createAxiosDataProvider";
|
||||
|
||||
@ -59,7 +59,7 @@ const onResponseError = (error: AxiosError): Promise<AxiosError> => {
|
||||
break;
|
||||
case 401:
|
||||
console.error("UnAuthorized");
|
||||
//return (window.location.href = "/logout");
|
||||
return (window.location.href = "/logout");
|
||||
break;
|
||||
case 403:
|
||||
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 "./useCustomDialog";
|
||||
export * from "./useDataSource";
|
||||
export * from "./useDataTable";
|
||||
export * from "./useLocalization";
|
||||
export * from "./usePagination";
|
||||
export * from "./useTheme";
|
||||
export * from "./useUnsavedChangesNotifier";
|
||||
|
||||
@ -7,7 +7,7 @@ export type SuccessNotificationResponse = {
|
||||
|
||||
export type PermissionResponse = unknown;
|
||||
|
||||
export type IdentityResponse = IIdentity_Response_DTO;
|
||||
export type IdentityResponse = IIdentity_Response_DTO | null;
|
||||
|
||||
export type AuthActionCheckResponse = {
|
||||
authenticated: boolean;
|
||||
|
||||
@ -28,6 +28,7 @@ export const AuthProvider = ({
|
||||
};
|
||||
|
||||
const handleCheck = async () => {
|
||||
console.trace("check");
|
||||
try {
|
||||
return Promise.resolve(authActions.check?.());
|
||||
} catch (error) {
|
||||
|
||||
@ -9,6 +9,7 @@ export const useIsLoggedIn = (queryOptions?: UseQueryOptions<AuthActionCheckResp
|
||||
const result = useQuery<AuthActionCheckResponse>({
|
||||
queryKey: keys().auth().action("check").get(),
|
||||
queryFn: check,
|
||||
retry: false,
|
||||
...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 {
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseQueryResult,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
import { QueryFunction, QueryKey, UseQueryResult, useQuery } from "@tanstack/react-query";
|
||||
|
||||
export interface IUseManyQueryOptions<
|
||||
TUseManyQueryData = unknown,
|
||||
TUseManyQueryError = unknown
|
||||
TUseManyQueryData = unknown,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TUseManyQueryError = unknown
|
||||
> {
|
||||
queryKey: QueryKey;
|
||||
queryFn: QueryFunction<TUseManyQueryData, QueryKey>;
|
||||
enabled?: boolean;
|
||||
select?: (data: TUseManyQueryData) => TUseManyQueryData;
|
||||
queryOptions?: any;
|
||||
queryKey: QueryKey;
|
||||
queryFn: QueryFunction<TUseManyQueryData, QueryKey>;
|
||||
enabled?: boolean;
|
||||
select?: (data: TUseManyQueryData) => TUseManyQueryData;
|
||||
queryOptions?: any;
|
||||
}
|
||||
|
||||
export function useMany<TUseManyQueryData, TUseManyQueryError>(
|
||||
options: IUseManyQueryOptions<TUseManyQueryData, TUseManyQueryError>
|
||||
options: IUseManyQueryOptions<TUseManyQueryData, TUseManyQueryError>
|
||||
): UseQueryResult<TUseManyQueryData, TUseManyQueryError> {
|
||||
const { queryKey, queryFn, enabled, select, queryOptions } = options;
|
||||
|
||||
const queryResponse = useQuery<TUseManyQueryData, TUseManyQueryError>({
|
||||
queryKey,
|
||||
queryFn,
|
||||
keepPreviousData: true,
|
||||
...queryOptions,
|
||||
enabled,
|
||||
select,
|
||||
const { queryKey, queryFn, enabled, select, queryOptions } = options;
|
||||
|
||||
});
|
||||
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<
|
||||
TUseRemoveMutationData,
|
||||
TUseRemoveMutationError,
|
||||
TUseRemoveMutationVariables
|
||||
TUseRemoveMutationData,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TUseRemoveMutationError,
|
||||
TUseRemoveMutationVariables
|
||||
> {
|
||||
mutationFn: (
|
||||
variables: TUseRemoveMutationVariables,
|
||||
) => Promise<TUseRemoveMutationData>;
|
||||
mutationFn: (variables: TUseRemoveMutationVariables) => Promise<TUseRemoveMutationData>;
|
||||
}
|
||||
|
||||
export function useRemove<
|
||||
TUseRemoveMutationData,
|
||||
TUseRemoveMutationError,
|
||||
TUseRemoveMutationVariables
|
||||
>(
|
||||
options: IUseRemoveMutationOptions<
|
||||
TUseRemoveMutationData,
|
||||
TUseRemoveMutationError,
|
||||
TUseRemoveMutationVariables>(options: IUseRemoveMutationOptions<
|
||||
TUseRemoveMutationData,
|
||||
TUseRemoveMutationError,
|
||||
TUseRemoveMutationVariables
|
||||
>) {
|
||||
const { mutationFn, ...params } = options;
|
||||
TUseRemoveMutationVariables
|
||||
>
|
||||
) {
|
||||
const { mutationFn, ...params } = options;
|
||||
|
||||
return useMutation<
|
||||
TUseRemoveMutationData,
|
||||
TUseRemoveMutationError,
|
||||
TUseRemoveMutationVariables>({
|
||||
mutationFn,
|
||||
...params
|
||||
});
|
||||
return useMutation<TUseRemoveMutationData, TUseRemoveMutationError, 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 { PropsWithChildren, createContext, useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
Dispatch,
|
||||
PropsWithChildren,
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useSyncedPagination } from "./useSyncedPagination";
|
||||
|
||||
export interface IDataTableContextState {
|
||||
pagination: PaginationState;
|
||||
setPagination: (newPagination: PaginationState) => void;
|
||||
sorting: [];
|
||||
setSorting: () => void;
|
||||
sorting: SortingState;
|
||||
setSorting: Dispatch<SetStateAction<SortingState>>;
|
||||
globalFilter: string;
|
||||
setGlobalFilter: (newGlobalFilter: string) => void;
|
||||
resetGlobalFilter: () => void;
|
||||
@ -16,12 +25,14 @@ export interface IDataTableContextState {
|
||||
export const DataTableContext = createContext<IDataTableContextState | null>(null);
|
||||
|
||||
export const DataTableProvider = ({
|
||||
syncWithLocation = true,
|
||||
initialGlobalFilter = "",
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
syncWithLocation?: boolean;
|
||||
initialGlobalFilter?: string;
|
||||
}>) => {
|
||||
const [pagination, setPagination] = usePaginationParams();
|
||||
const [pagination, setPagination] = useSyncedPagination(syncWithLocation);
|
||||
const [globalFilter, setGlobalFilter] = useState<string>(initialGlobalFilter);
|
||||
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 {
|
||||
DataTableColumnProps,
|
||||
getDataTableSelectionColumn,
|
||||
} from "@/components";
|
||||
import { DataTableColumnProps, getDataTableSelectionColumn } from "@/components";
|
||||
import { IListResponse_DTO } from "@shared/contexts";
|
||||
import {
|
||||
OnChangeFn,
|
||||
@ -12,12 +9,9 @@ import {
|
||||
} from "@tanstack/react-table";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { UseListQueryResult } from "../useDataSource";
|
||||
import { usePaginationParams } from "../usePagination";
|
||||
import { usePaginationSyncWithLocation } from "../usePagination";
|
||||
|
||||
type TUseDataTableQueryResult<TData, TError> = UseListQueryResult<
|
||||
IListResponse_DTO<TData>,
|
||||
TError
|
||||
>;
|
||||
type TUseDataTableQueryResult<TData, TError> = UseListQueryResult<IListResponse_DTO<TData>, TError>;
|
||||
|
||||
type TUseDataTableQuery<TData, TError> = (params: {
|
||||
pagination: {
|
||||
@ -44,16 +38,9 @@ type DataTableColumnsOptionsProps<TData, TValue> = {
|
||||
columns: DataTableColumnsProps<TData, TValue>;
|
||||
};
|
||||
|
||||
type DataTableColumnsProps<TData, TValue> = DataTableColumnProps<
|
||||
TData,
|
||||
TValue
|
||||
>[];
|
||||
type DataTableColumnsProps<TData, TValue> = DataTableColumnProps<TData, TValue>[];
|
||||
|
||||
export const useQueryDataTable = <
|
||||
TData = unknown,
|
||||
TValue = unknown,
|
||||
TError = Error
|
||||
>({
|
||||
export const useQueryDataTable = <TData = unknown, TValue = unknown, TError = Error>({
|
||||
fetchQuery,
|
||||
enabled = true,
|
||||
|
||||
@ -64,7 +51,7 @@ export const useQueryDataTable = <
|
||||
const defaultData = useMemo(() => [], []);
|
||||
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
const [pagination, setPagination] = usePaginationParams();
|
||||
const [pagination, setPagination] = usePaginationSyncWithLocation();
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
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 */
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LocaleToCurrencyTable, rtlLangsList } from "./utils";
|
||||
|
||||
type UseLocalizationProps = {
|
||||
@ -12,10 +11,10 @@ export const useLocalization = (props: UseLocalizationProps) => {
|
||||
const { locale } = props;
|
||||
const [lang, loc] = locale.split("-");
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
//const { i18n } = useTranslation();
|
||||
|
||||
// Obtener el idioma actual
|
||||
const currentLanguage = i18n.language;
|
||||
// const currentLanguage = i18n.language;
|
||||
|
||||
const formatCurrency = useCallback(
|
||||
(value: number) => {
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./usePagination";
|
||||
export * from "./usePaginationParams";
|
||||
export * from "./usePaginationSyncWithLocation";
|
||||
|
||||
@ -9,7 +9,7 @@ import { useMemo } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { usePagination } from "./usePagination";
|
||||
|
||||
export const usePaginationParams = (
|
||||
export const usePaginationSyncWithLocation = (
|
||||
initialPageIndex: number = INITIAL_PAGE_INDEX,
|
||||
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 "./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 { useCustomDialog } from "../useCustomDialog";
|
||||
import { CustomDialog } from "@/components";
|
||||
import { t } from "i18next";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useWarnAboutChange } from "./useWarnAboutChange";
|
||||
|
||||
type UnsavedChangesNotifierProps = {
|
||||
translationKey?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export const UnsavedChangesNotifier: React.FC<UnsavedChangesNotifierProps> = () => {
|
||||
const { openDialog: openWarmDialog, DialogComponent: WarmDialog } = useCustomDialog({
|
||||
title: "Hay cambios sin guardar",
|
||||
description: "Are you sure you want to leave? You have unsaved changes.",
|
||||
export const UnsavedChangesNotifier = ({
|
||||
message = t("unsaved_changes_prompt"),
|
||||
}: UnsavedChangesNotifierProps) => {
|
||||
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_tooltip": "Duplica las fila(s) seleccionadas(s)",
|
||||
"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": {
|
||||
"home": "Inicio",
|
||||
@ -146,6 +147,9 @@
|
||||
"desc": "desc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
"title": "Cotización"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
||||
@ -15,6 +15,7 @@ export const useAutosizeTextArea = ({
|
||||
minHeight = 0,
|
||||
}: UseAutosizeTextAreaProps) => {
|
||||
const [init, setInit] = React.useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
// We need to reset the height momentarily to get the correct scrollHeight for the textarea
|
||||
const offsetBorder = 2;
|
||||
@ -36,5 +37,5 @@ export const useAutosizeTextArea = ({
|
||||
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 {
|
||||
Collection,
|
||||
Currency,
|
||||
CurrencyData,
|
||||
Description,
|
||||
DomainError,
|
||||
ICreateQuote_Request_DTO,
|
||||
IDomainError,
|
||||
Language,
|
||||
Note,
|
||||
Percentage,
|
||||
Quantity,
|
||||
Result,
|
||||
UTCDateValue,
|
||||
@ -153,7 +154,7 @@ export class CreateQuoteUseCase
|
||||
return Result.fail(customerOrError.error);
|
||||
}
|
||||
|
||||
const currencyOrError = Currency.createFromCode(quoteDTO.currency_code);
|
||||
const currencyOrError = CurrencyData.createFromCode(quoteDTO.currency_code);
|
||||
if (currencyOrError.isFailure) {
|
||||
return Result.fail(currencyOrError.error);
|
||||
}
|
||||
@ -177,13 +178,15 @@ export class CreateQuoteUseCase
|
||||
quoteDTO.items?.map(
|
||||
(item) =>
|
||||
QuoteItem.create({
|
||||
articleId: item.article_id,
|
||||
description: Description.create(item.description).object,
|
||||
quantity: Quantity.create({ amount: item.quantity, precision: 4 }).object,
|
||||
quantity: Quantity.create(item.quantity).object,
|
||||
unitPrice: UnitPrice.create({
|
||||
amount: item.unit_price.amount,
|
||||
currencyCode: item.unit_price.currency,
|
||||
currencyCode: item.unit_price.currency_code,
|
||||
precision: item.unit_price.precision,
|
||||
}).object,
|
||||
discount: Percentage.create(item.discount.amount).object,
|
||||
}).object
|
||||
)
|
||||
);
|
||||
|
||||
@ -7,10 +7,11 @@ import {
|
||||
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||
import { Result, UniqueID } from "@shared/contexts";
|
||||
import { IQuoteRepository } from "../../domain";
|
||||
import { Dealer, IQuoteRepository } from "../../domain";
|
||||
|
||||
import { IInfrastructureError } from "@/contexts/common/infrastructure";
|
||||
import { Quote } from "../../domain/entities/Quotes/Quote";
|
||||
import { ISalesContext } from "../../infrastructure";
|
||||
|
||||
export interface IGetQuoteUseCaseRequest extends IUseCaseRequest {
|
||||
id: UniqueID;
|
||||
@ -25,14 +26,12 @@ export class GetQuoteUseCase
|
||||
{
|
||||
private _adapter: ISequelizeAdapter;
|
||||
private _repositoryManager: IRepositoryManager;
|
||||
private _dealer?: Dealer;
|
||||
|
||||
constructor(props: { adapter: ISequelizeAdapter; repositoryManager: IRepositoryManager }) {
|
||||
this._adapter = props.adapter;
|
||||
this._repositoryManager = props.repositoryManager;
|
||||
}
|
||||
|
||||
private getRepositoryByName<T>(name: string) {
|
||||
return this._repositoryManager.getRepository<T>(name);
|
||||
constructor(context: ISalesContext) {
|
||||
this._adapter = context.adapter;
|
||||
this._repositoryManager = context.repositoryManager;
|
||||
this._dealer = context.dealer;
|
||||
}
|
||||
|
||||
async execute(request: IGetQuoteUseCaseRequest): Promise<GetQuoteResponseOrError> {
|
||||
@ -63,9 +62,7 @@ export class GetQuoteUseCase
|
||||
return Result.ok<Quote>(Quote!);
|
||||
} catch (error: unknown) {
|
||||
const _error = error as IInfrastructureError;
|
||||
return Result.fail(
|
||||
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Error al consultar el usuario", _error)
|
||||
);
|
||||
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Query error", _error));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -58,13 +58,7 @@ export class ListQuotesUseCase implements IUseCase<IListQuotesParams, Promise<Li
|
||||
} catch (error: unknown) {
|
||||
const _error = error as IInfrastructureError;
|
||||
console.trace(_error.message);
|
||||
return Result.fail(
|
||||
UseCaseError.create(
|
||||
UseCaseError.REPOSITORY_ERROR,
|
||||
"Error al listar las cotizaciones",
|
||||
_error
|
||||
)
|
||||
);
|
||||
return Result.fail(UseCaseError.create(UseCaseError.REPOSITORY_ERROR, "Query error", _error));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import {
|
||||
AggregateRoot,
|
||||
Currency,
|
||||
CurrencyData,
|
||||
ICollection,
|
||||
IDomainError,
|
||||
Language,
|
||||
MoneyValue,
|
||||
Note,
|
||||
Percentage,
|
||||
Result,
|
||||
UTCDateValue,
|
||||
UniqueID,
|
||||
@ -20,13 +22,17 @@ export interface IQuoteProps {
|
||||
reference: QuoteReference;
|
||||
customer: QuoteCustomer;
|
||||
language: Language;
|
||||
currency: Currency;
|
||||
currency: CurrencyData;
|
||||
paymentMethod: Note;
|
||||
notes: Note;
|
||||
validity: Note;
|
||||
|
||||
items: ICollection<QuoteItem>;
|
||||
|
||||
//subtotalPrice: MoneyValue;
|
||||
discount: Percentage;
|
||||
//totalPrice: MoneyValue;
|
||||
|
||||
dealerId: UniqueID;
|
||||
}
|
||||
|
||||
@ -38,10 +44,15 @@ export interface IQuote {
|
||||
reference: QuoteReference;
|
||||
customer: QuoteCustomer;
|
||||
language: Language;
|
||||
currency: Currency;
|
||||
currency: CurrencyData;
|
||||
paymentMethod: Note;
|
||||
notes: Note;
|
||||
validity: Note;
|
||||
|
||||
subtotalPrice: MoneyValue;
|
||||
discount: Percentage;
|
||||
totalPrice: MoneyValue;
|
||||
|
||||
items: ICollection<QuoteItem>;
|
||||
|
||||
dealerId: UniqueID;
|
||||
@ -60,6 +71,14 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
|
||||
|
||||
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) {
|
||||
super(props, id);
|
||||
|
||||
@ -113,4 +132,16 @@ export class Quote extends AggregateRoot<IQuoteProps> implements IQuote {
|
||||
get 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,
|
||||
IEntityProps,
|
||||
MoneyValue,
|
||||
Percentage,
|
||||
Quantity,
|
||||
Result,
|
||||
UniqueID,
|
||||
} from "@shared/contexts";
|
||||
|
||||
export interface IQuoteItemProps extends IEntityProps {
|
||||
articleId: string;
|
||||
description: Description; // Descripción del artículo o servicio
|
||||
quantity: Quantity; // Cantidad de unidades
|
||||
unitPrice: MoneyValue; // Precio unitario en la moneda de la factura
|
||||
// subtotalPrice: MoneyValue; // Precio unitario * Cantidad
|
||||
discount: Percentage; // % descuento
|
||||
// totalPrice: MoneyValue;
|
||||
}
|
||||
|
||||
export interface IQuoteItem {
|
||||
articleId: string;
|
||||
description: Description;
|
||||
quantity: Quantity;
|
||||
unitPrice: MoneyValue;
|
||||
subtotalPrice: MoneyValue;
|
||||
discount: Percentage;
|
||||
totalPrice: MoneyValue;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
get articleId(): string {
|
||||
return this.props.articleId;
|
||||
}
|
||||
|
||||
get description(): Description {
|
||||
return this.props.description;
|
||||
}
|
||||
@ -37,4 +50,16 @@ export class QuoteItem extends Entity<IQuoteItemProps> implements IQuoteItem {
|
||||
get unitPrice(): MoneyValue {
|
||||
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 { ISalesContext } from "../../../../../Sales.context";
|
||||
|
||||
@ -13,44 +17,38 @@ export const GetQuotePresenter: IGetQuotePresenter = {
|
||||
id: quote.id.toString(),
|
||||
status: quote.status.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(),
|
||||
subtotal: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
total: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
|
||||
payment_method: quote.paymentMethod.toString(),
|
||||
validity: quote.validity.toString(),
|
||||
notes: quote.notes.toString(),
|
||||
|
||||
subtotal_price: quote.subtotalPrice.toObject(),
|
||||
discount: quote.discount.toObject(),
|
||||
total_price: quote.totalPrice.toObject(),
|
||||
|
||||
items: quoteItemPresenter(quote.items, context),
|
||||
dealer_id: quote.dealerId.toString(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// 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.items.map((item: QuoteItem) => ({
|
||||
article_id: item.articleId,
|
||||
description: item.description.toString(),
|
||||
quantity: item.quantity.toString(),
|
||||
unit_measure: "",
|
||||
unit_price: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
subtotal: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
total: {
|
||||
amount: 0,
|
||||
precision: 2,
|
||||
currency: "EUR",
|
||||
},
|
||||
quantity: item.quantity.toObject(),
|
||||
unit_price: item.unitPrice.toObject(),
|
||||
subtotal_price: item.subtotalPrice.toObject(),
|
||||
discount: item.discount.toObject(),
|
||||
total_price: item.totalPrice.toObject(),
|
||||
}))
|
||||
: [];
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { ListQuotesUseCase } from "@/contexts/sales/application";
|
||||
import { registerQuoteRepository } from "@/contexts/sales/infrastructure/Quote.repository";
|
||||
import { ISalesContext } from "@/contexts/sales/infrastructure/Sales.context";
|
||||
import Express from "express";
|
||||
import { ListQuotesController } from "./ListQuotes.controller";
|
||||
import { ListQuotesPresenter } from "./presenter";
|
||||
@ -9,7 +10,7 @@ export const listQuotesController = (
|
||||
res: Express.Response,
|
||||
next: Express.NextFunction
|
||||
) => {
|
||||
const context = res.locals.context;
|
||||
const context: ISalesContext = res.locals.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 { IQuoteProps, Quote, QuoteCustomer, QuoteReference } from "../../domain";
|
||||
@ -33,18 +33,41 @@ class QuoteMapper
|
||||
|
||||
const props: IQuoteProps = {
|
||||
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),
|
||||
currency: this.mapsValue(source, "quote_currency", Currency.createFromCode),
|
||||
language: this.mapsValue(source, "quote_language", Language.createFromCode),
|
||||
customer: this.mapsValue(source, "customer", QuoteCustomer.create),
|
||||
currency: this.mapsValue(source, "currency_code", CurrencyData.createFromCode),
|
||||
language: this.mapsValue(source, "lang_code", Language.createFromCode),
|
||||
customer: this.mapsValue(source, "customer_information", QuoteCustomer.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),
|
||||
|
||||
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),
|
||||
};
|
||||
|
||||
@ -75,9 +98,9 @@ class QuoteMapper
|
||||
payment_method: source.paymentMethod.toPrimitive(),
|
||||
notes: source.notes.toPrimitive(),
|
||||
|
||||
discount: 0,
|
||||
subtotal: 0,
|
||||
total: 0,
|
||||
subtotal_price: source.subtotalPrice.toPrimitive(),
|
||||
discount: source.discount.toPrimitive(),
|
||||
total_price: source.totalPrice.toPrimitive(),
|
||||
|
||||
items,
|
||||
dealer_id: source.dealerId.toPrimitive(),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { ISalesContext } from "../Sales.context";
|
||||
import { Quote_Model } from "../sequelize";
|
||||
@ -23,15 +23,40 @@ class QuoteItemMapper
|
||||
const id = this.mapsValue(source, "item_id", UniqueID.create);
|
||||
|
||||
const props: IQuoteItemProps = {
|
||||
articleId: source.id_article,
|
||||
description: this.mapsValue(source, "description", Description.create),
|
||||
quantity: this.mapsValue(source, "quantity", Quantity.create),
|
||||
unitPrice: this.mapsValue(source, "unit_price", (unit_price) =>
|
||||
UnitPrice.create({
|
||||
MoneyValue.create({
|
||||
amount: unit_price,
|
||||
currencyCode: sourceParent.currency_code,
|
||||
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);
|
||||
@ -50,16 +75,16 @@ class QuoteItemMapper
|
||||
const { index, sourceParent } = params;
|
||||
|
||||
return {
|
||||
item_id: source.id.toString(),
|
||||
quote_id: sourceParent.id.toPrimitive(),
|
||||
position: index,
|
||||
item_id: "", //article_id: source.id.toPrimitive(),
|
||||
id_article: source.articleId,
|
||||
description: source.description.toPrimitive(),
|
||||
quantity: source.quantity.toPrimitive(),
|
||||
unit_price: source.unitPrice.toPrimitive(),
|
||||
subtotal: 0,
|
||||
total: 0,
|
||||
//subtotal: source.calculateSubtotal().toPrimitive(),
|
||||
//total: source.calculateTotal().toPrimitive(),
|
||||
subtotal_price: source.subtotalPrice.toPrimitive(),
|
||||
discount: source.discount.toPrimitive(),
|
||||
total_price: source.totalPrice.toPrimitive(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,9 +50,9 @@ export class Quote_Model extends Model<
|
||||
declare notes: CreationOptional<string>;
|
||||
declare validity: CreationOptional<string>;
|
||||
|
||||
declare subtotal: CreationOptional<number>;
|
||||
declare subtotal_price: CreationOptional<number>;
|
||||
declare discount: CreationOptional<number>;
|
||||
declare total: CreationOptional<number>;
|
||||
declare total_price: CreationOptional<number>;
|
||||
|
||||
declare items: NonAttribute<QuoteItem_Model[]>;
|
||||
declare dealer: NonAttribute<Dealer_Model>;
|
||||
@ -108,7 +108,7 @@ export default (sequelize: Sequelize) => {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
|
||||
subtotal: {
|
||||
subtotal_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
@ -118,7 +118,7 @@ export default (sequelize: Sequelize) => {
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
total: {
|
||||
total_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
@ -30,12 +30,14 @@ export class QuoteItem_Model extends Model<
|
||||
|
||||
declare quote_id: string;
|
||||
declare item_id: string;
|
||||
declare id_article: string; // number ??
|
||||
declare position: number;
|
||||
declare description: CreationOptional<string>;
|
||||
declare quantity: CreationOptional<number>;
|
||||
declare unit_price: CreationOptional<number>;
|
||||
declare subtotal: CreationOptional<number>;
|
||||
declare total: CreationOptional<number>;
|
||||
declare subtotal_price: CreationOptional<number>;
|
||||
declare discount: CreationOptional<number>;
|
||||
declare total_price: CreationOptional<number>;
|
||||
|
||||
declare quote: NonAttribute<Quote_Model>;
|
||||
}
|
||||
@ -51,6 +53,10 @@ export default (sequelize: Sequelize) => {
|
||||
type: new DataTypes.UUID(),
|
||||
primaryKey: true,
|
||||
},
|
||||
id_article: {
|
||||
type: DataTypes.BIGINT().UNSIGNED,
|
||||
allowNull: false,
|
||||
},
|
||||
position: {
|
||||
type: new DataTypes.MEDIUMINT(),
|
||||
autoIncrement: false,
|
||||
@ -68,11 +74,15 @@ export default (sequelize: Sequelize) => {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
subtotal: {
|
||||
subtotal_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
total: {
|
||||
discount: {
|
||||
type: DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
total_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { checkUser } from "@/contexts/auth";
|
||||
import {
|
||||
createQuoteController,
|
||||
getQuoteController,
|
||||
listQuotesController,
|
||||
updateQuoteController,
|
||||
} from "@/contexts/sales/infrastructure/express/controllers";
|
||||
import { getDealerMiddleware } from "@/contexts/sales/infrastructure/express/middlewares/dealerMiddleware";
|
||||
import Express from "express";
|
||||
@ -10,11 +12,11 @@ export const QuoteRouter = (appRouter: Express.Router) => {
|
||||
const quoteRoutes: Express.Router = Express.Router({ mergeParams: true });
|
||||
|
||||
quoteRoutes.get("/", checkUser, getDealerMiddleware, listQuotesController);
|
||||
quoteRoutes.get("/:quoteId", checkUser, getDealerMiddleware, getQuoteController);
|
||||
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.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 {
|
||||
id: string;
|
||||
@ -10,5 +10,5 @@ export interface IListArticles_Response_DTO {
|
||||
|
||||
description: string;
|
||||
points: number;
|
||||
retail_price: IMoney_Response_DTO;
|
||||
retail_price: IQuantuty_Response_DTO;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {
|
||||
Currency,
|
||||
CurrencyData,
|
||||
Description,
|
||||
Email,
|
||||
Language,
|
||||
@ -46,7 +46,7 @@ export const ensureDateIsValid = (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);
|
||||
};
|
||||
|
||||
@ -4,14 +4,14 @@ import { Result, RuleValidator } from "../../domain";
|
||||
export interface IMoney_Request_DTO {
|
||||
amount: number;
|
||||
precision: number;
|
||||
currency: string;
|
||||
currency_code: string;
|
||||
}
|
||||
|
||||
export function ensureMoney_DTOIsValid(money: IMoney_Request_DTO) {
|
||||
const schema = Joi.object({
|
||||
amount: Joi.number(),
|
||||
precision: Joi.number(),
|
||||
currency: Joi.string(),
|
||||
currency_code: Joi.string(),
|
||||
});
|
||||
|
||||
const result = RuleValidator.validate<IMoney_Request_DTO>(schema, money);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export interface IPercentage_DTO {
|
||||
amount: number;
|
||||
precision: number;
|
||||
amount: number;
|
||||
precision: number;
|
||||
}
|
||||
|
||||
export interface IPercentage_Request_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 "./IMoney.dto";
|
||||
export * from "./IPercentage.dto";
|
||||
export * from "./IQuantity.dto";
|
||||
export * from "./ITaxType.dto";
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
export interface ICollection<T> {
|
||||
items: T[];
|
||||
totalCount: number;
|
||||
|
||||
toArray(): 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(
|
||||
(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) {
|
||||
this._items = new Map<number, T>(
|
||||
initialValues
|
||||
? initialValues.map(
|
||||
(value: any, index: number) => [index, value] as [number, T],
|
||||
)
|
||||
: [],
|
||||
? initialValues.map((value: any, index: number) => [index, value] as [number, T])
|
||||
: []
|
||||
);
|
||||
|
||||
this._totalCountIsProvided = typeof totalCount === "number";
|
||||
@ -89,9 +89,7 @@ export class Collection<T> implements ICollection<T> {
|
||||
}
|
||||
}
|
||||
|
||||
public find(
|
||||
predicate: (value: T, index: number, obj: T[]) => unknown,
|
||||
): T | undefined {
|
||||
public find(predicate: (value: T, index: number, obj: T[]) => unknown): T | undefined {
|
||||
return Array.from(this._items.values()).find(predicate);
|
||||
}
|
||||
|
||||
@ -106,8 +104,8 @@ export class Collection<T> implements ICollection<T> {
|
||||
}
|
||||
|
||||
public toStringArray(): string[] {
|
||||
return Array.from(this._items.values(), (element) =>
|
||||
JSON.stringify(element),
|
||||
).filter((element) => element.length > 0);
|
||||
return Array.from(this._items.values(), (element) => JSON.stringify(element)).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 { Result } from "../Result";
|
||||
|
||||
export interface ICurrency {
|
||||
export interface ICurrencyData {
|
||||
symbol: string;
|
||||
name: string;
|
||||
symbol_native: string;
|
||||
@ -15,9 +15,9 @@ export interface ICurrency {
|
||||
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 CURRENCIES = Currencies;
|
||||
|
||||
@ -29,7 +29,7 @@ export class Currency extends NullableValueObject<ICurrency> {
|
||||
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(
|
||||
RuleValidator.RULE_ALLOW_EMPTY.default(""),
|
||||
Joi.string()
|
||||
@ -41,24 +41,24 @@ export class Currency extends NullableValueObject<ICurrency> {
|
||||
return RuleValidator.validate<string>(rule, value);
|
||||
}
|
||||
|
||||
public static createFromCode(currencyCode: string, options: ICurrencyOptions = {}) {
|
||||
public static createFromCode(currencyCode: string, options: ICurrencyDataOptions = {}) {
|
||||
const _options = {
|
||||
...options,
|
||||
label: options.label ? options.label : "current_code",
|
||||
};
|
||||
|
||||
const validationResult = Currency.validate(currencyCode, _options);
|
||||
const validationResult = CurrencyData.validate(currencyCode, _options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(
|
||||
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() {
|
||||
return Currency.createFromCode(Currency.DEFAULT_CURRENCY_CODE);
|
||||
return CurrencyData.createFromCode(CurrencyData.DEFAULT_CURRENCY_CODE);
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
@ -0,0 +1 @@
|
||||
export * from "./CurrencyData";
|
||||
@ -1,27 +1,28 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
import DineroFactory, { Dinero } from "dinero.js";
|
||||
|
||||
import { Currency } from "./Currency";
|
||||
|
||||
import Joi from "joi";
|
||||
import { isNull } from "lodash";
|
||||
import { NullOr } from "../../../../utilities";
|
||||
import { RuleValidator } from "../RuleValidator";
|
||||
import { CurrencyData } from "./CurrencyData";
|
||||
import { Result } from "./Result";
|
||||
import { IValueObjectOptions, ValueObject } from "./ValueObject";
|
||||
|
||||
export interface IMoneyValueOptions extends IValueObjectOptions {
|
||||
defaultValue?: number;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export const defaultMoneyValueOptions: IMoneyValueOptions = {
|
||||
defaultValue: 0,
|
||||
locale: "es-ES",
|
||||
};
|
||||
|
||||
export interface MoneyValueObject {
|
||||
amount: number;
|
||||
precision: number;
|
||||
currency: string;
|
||||
currency_code: string;
|
||||
}
|
||||
|
||||
type RoundingMode =
|
||||
@ -43,7 +44,7 @@ export interface IMoneyValueProps {
|
||||
|
||||
const defaultMoneyValueProps = {
|
||||
amount: 0,
|
||||
currencyCode: Currency.DEFAULT_CURRENCY_CODE,
|
||||
currencyCode: CurrencyData.DEFAULT_CURRENCY_CODE,
|
||||
precision: 2,
|
||||
};
|
||||
|
||||
@ -56,13 +57,10 @@ interface IMoneyValue {
|
||||
isNull(): boolean;
|
||||
|
||||
getAmount(): number;
|
||||
getCurrency(): Currency;
|
||||
getCurrency(): CurrencyData;
|
||||
getLocale(): string;
|
||||
getPrecision(): number;
|
||||
convertPrecision(
|
||||
newPrecision: number,
|
||||
roundingMode?: RoundingMode,
|
||||
): MoneyValue;
|
||||
convertPrecision(newPrecision: number, roundingMode?: RoundingMode): MoneyValue;
|
||||
|
||||
add(addend: MoneyValue): MoneyValue;
|
||||
subtract(subtrahend: MoneyValue): MoneyValue;
|
||||
@ -90,16 +88,16 @@ interface 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 MAX_VALUE = Number.MAX_VALUE;
|
||||
|
||||
private readonly _isNull: boolean;
|
||||
private readonly _options: IMoneyValueOptions;
|
||||
|
||||
protected static validate(
|
||||
amount: NullOr<number | string>,
|
||||
options: IMoneyValueOptions,
|
||||
) {
|
||||
protected static validate(amount: NullOr<number | string>, options: IMoneyValueOptions) {
|
||||
const ruleNull = Joi.any()
|
||||
.optional() // <- undefined
|
||||
.valid(null); // <- null
|
||||
@ -131,7 +129,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
|
||||
public static create(
|
||||
props: IMoneyValueProps = defaultMoneyValueProps,
|
||||
options = defaultMoneyValueOptions,
|
||||
options = defaultMoneyValueOptions
|
||||
) {
|
||||
if (props === null) {
|
||||
throw new Error(`InvalidParams: props params is missing`);
|
||||
@ -141,7 +139,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
amount = defaultMoneyValueProps.amount,
|
||||
currencyCode = defaultMoneyValueProps.currencyCode,
|
||||
precision = defaultMoneyValueProps.precision,
|
||||
} = props;
|
||||
} = props || {};
|
||||
|
||||
const validationResult = MoneyValue.validate(amount, options);
|
||||
|
||||
@ -149,13 +147,11 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
return Result.fail(validationResult.error);
|
||||
}
|
||||
|
||||
const _amount: NullOr<number> = MoneyValue.sanitize(
|
||||
validationResult.object,
|
||||
);
|
||||
const _amount: NullOr<number> = MoneyValue.sanitize(validationResult.object);
|
||||
|
||||
const prop = DineroFactory({
|
||||
amount: !isNull(_amount) ? _amount : 0,
|
||||
currency: Currency.DEFAULT_CURRENCY_CODE,
|
||||
amount: !isNull(_amount) ? _amount : options.defaultValue,
|
||||
currency: CurrencyData.createFromCode(currencyCode).object.code,
|
||||
precision,
|
||||
}).setLocale(options.locale);
|
||||
|
||||
@ -175,29 +171,23 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
}
|
||||
|
||||
protected static createFromDinero(dinero: Dinero) {
|
||||
return Result.ok<MoneyValue>(
|
||||
new MoneyValue(dinero, false, defaultMoneyValueOptions),
|
||||
return Result.ok<MoneyValue>(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 {
|
||||
return MoneyValue.createFromDinero(
|
||||
DineroFactory.minimum(objects.map((object) => object.props)),
|
||||
).object;
|
||||
return MoneyValue.createFromDinero(DineroFactory.minimum(objects.map((object) => object.props)))
|
||||
.object;
|
||||
}
|
||||
|
||||
public static maximum(objects: ReadonlyArray<MoneyValue>): MoneyValue {
|
||||
return MoneyValue.createFromDinero(
|
||||
DineroFactory.maximum(objects.map((object) => object.props)),
|
||||
).object;
|
||||
return MoneyValue.createFromDinero(DineroFactory.maximum(objects.map((object) => object.props)))
|
||||
.object;
|
||||
}
|
||||
|
||||
constructor(value: Dinero, isNull: boolean, options: IMoneyValueOptions) {
|
||||
@ -238,17 +228,13 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
return this.props.getPrecision();
|
||||
}
|
||||
|
||||
public convertPrecision(
|
||||
newPrecision: number,
|
||||
roundingMode?: RoundingMode,
|
||||
): MoneyValue {
|
||||
return MoneyValue.createFromDinero(
|
||||
this.props.convertPrecision(newPrecision, roundingMode),
|
||||
).object;
|
||||
public convertPrecision(newPrecision: number, roundingMode?: RoundingMode): MoneyValue {
|
||||
return MoneyValue.createFromDinero(this.props.convertPrecision(newPrecision, roundingMode))
|
||||
.object;
|
||||
}
|
||||
|
||||
public getCurrency(): Currency {
|
||||
return Currency.createFromCode(this.props.getCurrency()).object;
|
||||
public getCurrency(): CurrencyData {
|
||||
return CurrencyData.createFromCode(this.props.getCurrency()).object;
|
||||
}
|
||||
|
||||
public getLocale(): string {
|
||||
@ -260,34 +246,23 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
}
|
||||
|
||||
public subtract(subtrahend: MoneyValue): MoneyValue {
|
||||
return MoneyValue.createFromDinero(this.props.subtract(subtrahend.props))
|
||||
.object;
|
||||
return MoneyValue.createFromDinero(this.props.subtract(subtrahend.props)).object;
|
||||
}
|
||||
|
||||
public multiply(multiplier: number, roundingMode?: RoundingMode): MoneyValue {
|
||||
return MoneyValue.createFromDinero(
|
||||
this.props.multiply(multiplier, roundingMode),
|
||||
).object;
|
||||
return MoneyValue.createFromDinero(this.props.multiply(multiplier, roundingMode)).object;
|
||||
}
|
||||
|
||||
public divide(divisor: number, roundingMode?: RoundingMode): MoneyValue {
|
||||
return MoneyValue.createFromDinero(this.props.divide(divisor, roundingMode))
|
||||
.object;
|
||||
return MoneyValue.createFromDinero(this.props.divide(divisor, roundingMode)).object;
|
||||
}
|
||||
|
||||
public percentage(
|
||||
percentage: number,
|
||||
roundingMode?: RoundingMode,
|
||||
): MoneyValue {
|
||||
return MoneyValue.createFromDinero(
|
||||
this.props.percentage(percentage, roundingMode),
|
||||
).object;
|
||||
public percentage(percentage: number, roundingMode?: RoundingMode): MoneyValue {
|
||||
return MoneyValue.createFromDinero(this.props.percentage(percentage, roundingMode)).object;
|
||||
}
|
||||
|
||||
public allocate(ratios: ReadonlyArray<number>): MoneyValue[] {
|
||||
return this.props
|
||||
.allocate(ratios)
|
||||
.map((dinero) => MoneyValue.createFromDinero(dinero).object);
|
||||
return this.props.allocate(ratios).map((dinero) => MoneyValue.createFromDinero(dinero).object);
|
||||
}
|
||||
|
||||
public equalsTo(comparator: MoneyValue): boolean {
|
||||
@ -347,7 +322,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
return {
|
||||
amount: obj.amount,
|
||||
precision: obj.precision,
|
||||
currency: String(obj.currency),
|
||||
currency_code: String(obj.currency),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,57 +1,160 @@
|
||||
import Joi from "joi";
|
||||
import { isNull } from "lodash";
|
||||
import { NullOr } from "../../../../utilities";
|
||||
import { RuleValidator } from "../RuleValidator";
|
||||
import {
|
||||
INullableValueObjectOptions,
|
||||
NullableValueObject,
|
||||
} from "./NullableValueObject";
|
||||
import { INullableValueObjectOptions, NullableValueObject } from "./NullableValueObject";
|
||||
import { Result } from "./Result";
|
||||
|
||||
export class Percentage extends NullableValueObject<number> {
|
||||
private static readonly MIN_VALUE = 0;
|
||||
private static readonly MAX_VALUE = 100;
|
||||
export interface IPercentageOptions extends INullableValueObjectOptions {}
|
||||
|
||||
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()
|
||||
.min(Percentage.MIN_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(
|
||||
value: number,
|
||||
options: INullableValueObjectOptions = {},
|
||||
props: IPercentageProps = defaultPercentageProps,
|
||||
options: IPercentageOptions = {}
|
||||
) {
|
||||
if (props === null) {
|
||||
throw new Error(`InvalidParams: props params is missing`);
|
||||
}
|
||||
|
||||
const { amount = defaultPercentageProps.amount, precision = defaultPercentageProps.precision } =
|
||||
props;
|
||||
|
||||
const _options = {
|
||||
label: "percentage",
|
||||
...options,
|
||||
};
|
||||
|
||||
const validationResult = Percentage.validate(value, _options);
|
||||
const validationResult = Percentage.validate(amount, _options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
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 {
|
||||
return this.isNull() ? 0 : Number(this.value);
|
||||
if (this.isNull()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const factor = Math.pow(10, this.precision);
|
||||
const amount = Number(this.amount) / factor;
|
||||
return Number(amount.toFixed(this.precision));
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.isNull() ? "" : String(this.value);
|
||||
return this.isNull() ? "" : String(this.toNumber());
|
||||
}
|
||||
|
||||
public toPrimitive(): number {
|
||||
return this.toNumber();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface QuantityObject {
|
||||
amount: number;
|
||||
precision: number;
|
||||
}
|
||||
|
||||
const defaultQuantityProps = {
|
||||
amount: 1,
|
||||
amount: 0,
|
||||
precision: 0,
|
||||
};
|
||||
|
||||
export class Quantity extends NullableValueObject<IQuantity> {
|
||||
public static readonly DEFAULT_PRECISION = defaultQuantityProps.precision;
|
||||
|
||||
private readonly _isNull: boolean;
|
||||
private readonly _options: IQuantityOptions;
|
||||
|
||||
protected static validate(value: NullOr<number | string>, options: IQuantityOptions = {}) {
|
||||
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(null);
|
||||
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(
|
||||
defaultQuantityProps.amount
|
||||
);
|
||||
|
||||
const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label(
|
||||
options.label ? options.label : "quantity"
|
||||
@ -100,6 +109,14 @@ export class Quantity extends NullableValueObject<IQuantity> {
|
||||
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();
|
||||
};
|
||||
@ -130,9 +147,9 @@ export class Quantity extends NullableValueObject<IQuantity> {
|
||||
return this.toObject();
|
||||
}
|
||||
|
||||
public toObject(): IQuantityProps {
|
||||
public toObject(): QuantityObject {
|
||||
return {
|
||||
amount: this.amount,
|
||||
amount: this.amount ? this.amount : 0,
|
||||
precision: this.precision,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export * from "./Address";
|
||||
export * from "./AggregateRoot";
|
||||
export * from "./Collection";
|
||||
export * from "./Currency";
|
||||
export * from "./CurrencyData";
|
||||
export * from "./Description";
|
||||
export * from "./Email";
|
||||
export * from "./Entity";
|
||||
@ -19,11 +19,11 @@ export * from "./Result";
|
||||
export * from "./ResultCollection";
|
||||
export * from "./Slug";
|
||||
export * from "./StringValueObject";
|
||||
export * from "./TINNumber";
|
||||
export * from "./TextValueObject";
|
||||
export * from "./UTCDateValue";
|
||||
export * from "./TINNumber";
|
||||
export * from "./UniqueID";
|
||||
export * from "./UnitPrice";
|
||||
//export * from "./UnitPrice";
|
||||
export * from "./UTCDateValue";
|
||||
export * from "./ValueObject";
|
||||
|
||||
export * from "./QueryCriteria";
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
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 {
|
||||
id: string;
|
||||
@ -13,14 +19,23 @@ export interface ICreateQuote_Request_DTO {
|
||||
notes: string;
|
||||
validity: string;
|
||||
|
||||
subtotal: IMoney_Response_DTO;
|
||||
discount: IPercentage_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
|
||||
items: ICreateQuoteItem_Request_DTO[];
|
||||
|
||||
dealer_id: string;
|
||||
}
|
||||
|
||||
export interface ICreateQuoteItem_Request_DTO {
|
||||
article_id: string;
|
||||
quantity: IQuantity_Response_DTO;
|
||||
description: string;
|
||||
quantity: string;
|
||||
unit_measure: string;
|
||||
unit_price: IMoney_Request_DTO;
|
||||
unit_price: IMoney_Response_DTO;
|
||||
price: IMoney_Response_DTO;
|
||||
discount: IPercentage_Response_DTO;
|
||||
total_price: IMoney_Response_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(
|
||||
Joi.object({
|
||||
article_id: Joi.string(),
|
||||
description: Joi.string(),
|
||||
quantity: Joi.string(),
|
||||
unit_measure: Joi.string(),
|
||||
quantity: {
|
||||
amount: Joi.number(),
|
||||
precision: Joi.number(),
|
||||
},
|
||||
unit_price: Joi.object({
|
||||
amount: Joi.number(),
|
||||
precision: Joi.number(),
|
||||
currency: Joi.string(),
|
||||
}),
|
||||
discount: Joi.object({
|
||||
amount: Joi.number(),
|
||||
precision: Joi.number(),
|
||||
}),
|
||||
}).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 {
|
||||
id: string;
|
||||
status: string;
|
||||
date: string;
|
||||
reference: string;
|
||||
customer_information: string;
|
||||
lang_code: string;
|
||||
currency_code: string;
|
||||
payment_method: string;
|
||||
notes: string;
|
||||
validity: string;
|
||||
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
|
||||
discount: IPercentage_Response_DTO;
|
||||
items: ICreateQuote_QuoteItem_Response_DTO[];
|
||||
}
|
||||
|
||||
export interface ICreateQuote_QuoteItem_Response_DTO {
|
||||
article_id: string;
|
||||
quantity: IQuantity_Response_DTO;
|
||||
description: string;
|
||||
quantity: string;
|
||||
unit_measure: string;
|
||||
unit_price: IMoney_Response_DTO;
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
price: 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 {
|
||||
id: string;
|
||||
@ -8,21 +12,26 @@ export interface IGetQuote_Response_DTO {
|
||||
customer_information: string;
|
||||
lang_code: string;
|
||||
currency_code: string;
|
||||
|
||||
payment_method: string;
|
||||
notes: string;
|
||||
validity: string;
|
||||
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
subtotal_price: IMoney_Response_DTO;
|
||||
discount: IPercentage_Response_DTO;
|
||||
total_price: IMoney_Response_DTO;
|
||||
|
||||
items: IGetQuote_QuoteItem_Response_DTO[];
|
||||
|
||||
dealer_id: string;
|
||||
}
|
||||
|
||||
export interface IGetQuote_QuoteItem_Response_DTO {
|
||||
article_id: string;
|
||||
quantity: IQuantity_Response_DTO;
|
||||
description: string;
|
||||
quantity: string;
|
||||
unit_measure: string;
|
||||
unit_price: IMoney_Response_DTO;
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
subtotal_price: 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 {
|
||||
id: string;
|
||||
@ -9,6 +9,6 @@ export interface IListQuotes_Response_DTO {
|
||||
lang_code: string;
|
||||
currency_code: string;
|
||||
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
subtotal: IQuantuty_Response_DTO;
|
||||
total: IQuantuty_Response_DTO;
|
||||
}
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
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 {
|
||||
status: string;
|
||||
@ -12,37 +20,71 @@ export interface IUpdateQuote_Request_DTO {
|
||||
notes: string;
|
||||
validity: string;
|
||||
|
||||
subtotal: IMoney_Request_DTO;
|
||||
discount: IPercentage_Request_DTO;
|
||||
items: IUpdateQuoteItem_Request_DTO[];
|
||||
}
|
||||
|
||||
export interface IUpdateQuoteItem_Request_DTO {
|
||||
article_id: string;
|
||||
quantity: IQuantity_Response_DTO;
|
||||
description: string;
|
||||
quantity: string;
|
||||
unit_measure: string;
|
||||
unit_price: IMoney_Request_DTO;
|
||||
unit_price: IMoney_Response_DTO;
|
||||
subtotal_price: IMoney_Response_DTO;
|
||||
discount: IPercentage_Response_DTO;
|
||||
total_price: IMoney_Response_DTO;
|
||||
}
|
||||
|
||||
export function ensureUpdateQuote_Request_DTOIsValid(quoteDTO: IUpdateQuote_Request_DTO) {
|
||||
const schema = Joi.object({
|
||||
status: Joi.string(),
|
||||
date: Joi.string(),
|
||||
reference: Joi.string(),
|
||||
lang_code: Joi.string(),
|
||||
customer_information: Joi.string(),
|
||||
lang_code: Joi.string(),
|
||||
currency_code: Joi.string(),
|
||||
payment_method: Joi.string(),
|
||||
notes: 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(
|
||||
Joi.object({
|
||||
article_id: Joi.string(),
|
||||
quantity: Joi.object({
|
||||
amount: Joi.number(),
|
||||
precision: Joi.number(),
|
||||
}),
|
||||
description: Joi.string(),
|
||||
quantity: Joi.string(),
|
||||
unit_measure: Joi.string(),
|
||||
unit_price: Joi.object({
|
||||
amount: Joi.number(),
|
||||
precision: Joi.number(),
|
||||
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);
|
||||
|
||||
@ -1,23 +1,33 @@
|
||||
import { IMoney_Response_DTO } from "shared/lib/contexts/common";
|
||||
import { IPercentage_Response_DTO, IQuantity_Response_DTO } from "../../../../../common";
|
||||
|
||||
export interface IUpdateQuote_Response_DTO {
|
||||
id: string;
|
||||
status: string;
|
||||
date: string;
|
||||
language_code: string;
|
||||
reference: string;
|
||||
customer_information: string;
|
||||
lang_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: IUpdateQuote_QuoteItem_Response_DTO[];
|
||||
|
||||
dealer_id: string;
|
||||
}
|
||||
|
||||
export interface IUpdateQuote_QuoteItem_Response_DTO {
|
||||
article_id: string;
|
||||
quantity: IQuantity_Response_DTO;
|
||||
description: string;
|
||||
quantity: string;
|
||||
unit_measure: string;
|
||||
unit_price: IMoney_Response_DTO;
|
||||
subtotal: IMoney_Response_DTO;
|
||||
total: IMoney_Response_DTO;
|
||||
price: IMoney_Response_DTO;
|
||||
discount: IPercentage_Response_DTO;
|
||||
total_price: IMoney_Response_DTO;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user