.
This commit is contained in:
parent
d4665a6de6
commit
740f5fd238
@ -273,7 +273,7 @@ export function SortableDataTable({ columns, data, actions }: SortableDataTableP
|
||||
description: "aaaa",
|
||||
unit_price: {
|
||||
amount: "10000",
|
||||
precision: 4,
|
||||
scale: 4,
|
||||
currency: "EUR",
|
||||
},
|
||||
discount: {
|
||||
|
||||
@ -12,7 +12,6 @@ import { t } from "i18next";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { useDetailColumns } from "../../hooks";
|
||||
import { CatalogPickerDataTable } from "../CatalogPickerDataTable";
|
||||
import { SortableDataTable } from "../SortableDataTable";
|
||||
|
||||
export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData }) => {
|
||||
@ -42,12 +41,13 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
|
||||
header: () => (
|
||||
<div className='text-right'>{t("quotes.form_fields.items.quantity.label")}</div>
|
||||
),
|
||||
size: 5,
|
||||
size: 8,
|
||||
cell: ({ row: { index } }) => {
|
||||
return (
|
||||
<FormQuantityField
|
||||
variant='outline'
|
||||
precision={Quantity.DEFAULT_PRECISION}
|
||||
scale={0}
|
||||
className='text-right'
|
||||
{...register(`items.${index}.quantity`)}
|
||||
/>
|
||||
);
|
||||
@ -73,14 +73,14 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
|
||||
<FormCurrencyField
|
||||
variant='outline'
|
||||
currency={currency}
|
||||
precision={4}
|
||||
scale={4}
|
||||
className='text-right'
|
||||
{...register(`items.${index}.unit_price`)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
/*{
|
||||
id: "subtotal_price" as const,
|
||||
accessorKey: "subtotal_price",
|
||||
header: () => (
|
||||
@ -91,34 +91,33 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
|
||||
<FormCurrencyField
|
||||
variant='outline'
|
||||
currency={currency}
|
||||
precision={4}
|
||||
scale={4}
|
||||
disabled
|
||||
className='text-right'
|
||||
{...register(`items.${index}.subtotal_price`)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
},*/
|
||||
{
|
||||
id: "discount" as const,
|
||||
accessorKey: "discount",
|
||||
size: 5,
|
||||
size: 8,
|
||||
header: () => (
|
||||
<div className='text-right'>{t("quotes.form_fields.items.discount.label")}</div>
|
||||
),
|
||||
cell: ({ row: { index }, column: { id } }) => {
|
||||
cell: ({ row: { index } }) => {
|
||||
return (
|
||||
<>
|
||||
<FormPercentageField
|
||||
variant='outline'
|
||||
precision={0}
|
||||
className='text-right'
|
||||
{...register(`items.${index}.discount`)}
|
||||
/>
|
||||
</>
|
||||
<FormPercentageField
|
||||
variant='outline'
|
||||
scale={2}
|
||||
className='text-right'
|
||||
{...register(`items.${index}.discount`)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
/*{
|
||||
id: "total_price" as const,
|
||||
accessorKey: "total_price",
|
||||
header: () => (
|
||||
@ -129,13 +128,14 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
|
||||
<FormCurrencyField
|
||||
variant='outline'
|
||||
currency={currency}
|
||||
precision={4}
|
||||
scale={4}
|
||||
disabled
|
||||
className='text-right'
|
||||
{...register(`items.${index}.total_price`)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
},*/
|
||||
],
|
||||
{
|
||||
enableDragHandleColumn: true,
|
||||
@ -177,8 +177,8 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
|
||||
fieldActions.append({
|
||||
...newArticle,
|
||||
quantity: {
|
||||
amount: 12,
|
||||
precision: Quantity.DEFAULT_PRECISION,
|
||||
amount: 100,
|
||||
scale: Quantity.DEFAULT_SCALE,
|
||||
},
|
||||
unit_price: newArticle.retail_price,
|
||||
});
|
||||
@ -216,7 +216,7 @@ export const QuoteDetailsCardEditor = ({ currency }: { currency: CurrencyData })
|
||||
<ResizableHandle withHandle className='mx-3' />
|
||||
<ResizablePanel defaultSize={defaultLayout[1]} minSize={10}>
|
||||
<DataTableProvider syncWithLocation={false}>
|
||||
<CatalogPickerDataTable onClick={handleInsertArticle} />
|
||||
{/*<CatalogPickerDataTable onClick={handleInsertArticle} />*/}
|
||||
</DataTableProvider>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
||||
@ -12,7 +12,6 @@ import { CurrencyData, IUpdateQuote_Request_DTO, MoneyValue } from "@shared/cont
|
||||
import { t } from "i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import useFormPersist from "react-hook-form-persist";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { QuoteDetailsCardEditor, QuoteGeneralCardEditor } from "./components/editors";
|
||||
@ -57,42 +56,46 @@ export const QuoteEdit = () => {
|
||||
validity: "",
|
||||
subtotal_price: {
|
||||
amount: undefined,
|
||||
precision: 2,
|
||||
scale: 2,
|
||||
currency_code: data?.currency_code,
|
||||
},
|
||||
discount: {
|
||||
amount: undefined,
|
||||
precision: 0,
|
||||
scale: 0,
|
||||
},
|
||||
total_price: {
|
||||
amount: undefined,
|
||||
precision: 2,
|
||||
scale: 2,
|
||||
currency_code: data?.currency_code,
|
||||
},
|
||||
items: [
|
||||
/*items: [
|
||||
{
|
||||
quantity: {
|
||||
amount: undefined,
|
||||
scale: 2,
|
||||
},
|
||||
subtotal_price: {
|
||||
amount: undefined,
|
||||
precision: 4,
|
||||
scale: 4,
|
||||
currency_code: data?.currency_code,
|
||||
},
|
||||
discount: {
|
||||
amount: undefined,
|
||||
precision: 0,
|
||||
scale: 0,
|
||||
},
|
||||
total_price: {
|
||||
amount: undefined,
|
||||
precision: 4,
|
||||
scale: 4,
|
||||
currency_code: data?.currency_code,
|
||||
},
|
||||
},
|
||||
],
|
||||
],*/
|
||||
},
|
||||
});
|
||||
|
||||
const { watch, getValues, setValue, formState } = form;
|
||||
|
||||
const { clear } = useFormPersist(
|
||||
/*const { clear } = useFormPersist(
|
||||
"quote-edit",
|
||||
{
|
||||
watch,
|
||||
@ -102,7 +105,7 @@ export const QuoteEdit = () => {
|
||||
storage: window.localStorage, // default window.sessionStorage
|
||||
//exclude: ['foo']
|
||||
}
|
||||
);
|
||||
);*/
|
||||
|
||||
const { isSubmitting } = formState;
|
||||
|
||||
@ -116,7 +119,7 @@ export const QuoteEdit = () => {
|
||||
//onSettled: () => {},
|
||||
onSuccess: () => {
|
||||
toast("Guardado!");
|
||||
clear();
|
||||
//clear();
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -139,18 +142,21 @@ export const QuoteEdit = () => {
|
||||
let quoteSubtotal = MoneyValue.create().object;
|
||||
|
||||
// Recálculo líneas
|
||||
items.map((item, index) => {
|
||||
const itemTotals = calculateItemTotals(item);
|
||||
items &&
|
||||
items.map((item, index) => {
|
||||
const itemTotals = calculateItemTotals(item);
|
||||
|
||||
if (itemTotals === null) {
|
||||
return;
|
||||
}
|
||||
console.log(itemTotals?.quantity.toObject());
|
||||
|
||||
quoteSubtotal = quoteSubtotal.add(itemTotals.totalPrice);
|
||||
if (itemTotals === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(`items.${index}.subtotal_price`, itemTotals.subtotalPrice.toObject());
|
||||
setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject());
|
||||
});
|
||||
quoteSubtotal = quoteSubtotal.add(itemTotals.totalPrice);
|
||||
|
||||
setValue(`items.${index}.subtotal_price`, itemTotals.subtotalPrice.toObject());
|
||||
setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject());
|
||||
});
|
||||
|
||||
// Recálculo completo
|
||||
setValue("subtotal_price", quoteSubtotal.toObject());
|
||||
|
||||
@ -3,7 +3,7 @@ import { FormControl, FormDescription, FormField, FormItem, InputProps } from "@
|
||||
import { CurrencyData, MoneyValue } from "@shared/contexts";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import CurrencyInput from "react-currency-input-field";
|
||||
import CurrencyInput, { CurrencyInputOnChangeValues } from "react-currency-input-field";
|
||||
import { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form";
|
||||
import { FormErrorMessage } from "./FormErrorMessage";
|
||||
import { FormLabel, FormLabelProps } from "./FormLabel";
|
||||
@ -38,7 +38,7 @@ export type FormCurrencyFieldProps<
|
||||
UseControllerProps<TFieldValues, TName> &
|
||||
VariantProps<typeof formCurrencyFieldVariants> & {
|
||||
currency: CurrencyData;
|
||||
precision: number;
|
||||
scale: number;
|
||||
};
|
||||
|
||||
export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrencyFieldProps>(
|
||||
@ -54,7 +54,7 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
|
||||
defaultValue,
|
||||
rules,
|
||||
readOnly,
|
||||
precision,
|
||||
scale,
|
||||
currency,
|
||||
variant,
|
||||
} = props;
|
||||
@ -68,22 +68,17 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
|
||||
}
|
||||
|
||||
const moneyOrError = MoneyValue.create(value);
|
||||
|
||||
if (moneyOrError.isFailure) {
|
||||
throw moneyOrError.error;
|
||||
}
|
||||
|
||||
return moneyOrError.object
|
||||
.convertPrecision(precision ?? value.precision)
|
||||
.toUnit()
|
||||
.toString();
|
||||
return moneyOrError.object.toString();
|
||||
},
|
||||
output: (value: string | undefined, name?: string, values?: CurrencyInputOnChangeValues) => {
|
||||
const { value: amount } = values ?? { value: null };
|
||||
|
||||
output: (value: string | undefined) => {
|
||||
const moneyOrError = MoneyValue.create({
|
||||
amount: value?.replace(",", "") ?? null,
|
||||
precision,
|
||||
currencyCode: currency.code,
|
||||
});
|
||||
const moneyOrError = MoneyValue.createFromFormattedValue(amount, currency.code);
|
||||
if (moneyOrError.isFailure) {
|
||||
throw moneyOrError.error;
|
||||
}
|
||||
@ -118,11 +113,17 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
|
||||
groupSeparator='.'
|
||||
decimalSeparator=','
|
||||
placeholder={placeholder}
|
||||
//fixedDecimalLength={precision} <- no activar para que sea más cómodo escribir las cantidades
|
||||
decimalsLimit={precision}
|
||||
decimalScale={precision}
|
||||
//allowDecimals={scale !== 0}
|
||||
decimalsLimit={scale}
|
||||
decimalScale={scale}
|
||||
//fixedDecimalLength={scale} <- no activar para que sea más cómodo escribir las cantidades
|
||||
step={1}
|
||||
// { ...field }
|
||||
value={transform.input(field.value)}
|
||||
onValueChange={(e) => field.onChange(transform.output(e))}
|
||||
//onChange={() => {}}
|
||||
onValueChange={(value, name, values) =>
|
||||
field.onChange(transform.output(value, name, values))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
{description && <FormDescription>{description}</FormDescription>}
|
||||
|
||||
@ -1,147 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FormControl, FormDescription, FormMessage, Input } from "@/ui";
|
||||
|
||||
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"> & {
|
||||
defaultValue?: any;
|
||||
};
|
||||
|
||||
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: 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 = 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 (
|
||||
<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 (
|
||||
<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 && (
|
||||
<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
|
||||
placeholder={placeholder}
|
||||
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'>
|
||||
{createElement(
|
||||
trailIcon,
|
||||
{
|
||||
className: "h-5 w-5 text-muted-foreground",
|
||||
"aria-hidden": true,
|
||||
},
|
||||
null
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{button && <>{createElement(button)}</>}
|
||||
</div>
|
||||
{description && <FormDescription>{description}</FormDescription>}
|
||||
<FormMessage />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -4,7 +4,7 @@ import { cn } from "@/lib/utils";
|
||||
import { FormControl, FormDescription, FormField, FormItem, InputProps } from "@/ui";
|
||||
import { Percentage } from "@shared/contexts";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import CurrencyInput from "react-currency-input-field";
|
||||
import CurrencyInput, { CurrencyInputOnChangeValues } from "react-currency-input-field";
|
||||
import { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form";
|
||||
import { FormErrorMessage } from "./FormErrorMessage";
|
||||
import { FormLabel, FormLabelProps } from "./FormLabel";
|
||||
@ -39,7 +39,7 @@ export type FormPercentageFieldProps<
|
||||
FormInputWithIconProps &
|
||||
UseControllerProps<TFieldValues, TName> &
|
||||
VariantProps<typeof formPercentageFieldVariants> & {
|
||||
precision: number;
|
||||
scale: number;
|
||||
};
|
||||
|
||||
export const FormPercentageField = React.forwardRef<
|
||||
@ -57,7 +57,7 @@ export const FormPercentageField = React.forwardRef<
|
||||
defaultValue,
|
||||
rules,
|
||||
readOnly,
|
||||
precision,
|
||||
scale,
|
||||
variant,
|
||||
} = props;
|
||||
|
||||
@ -74,18 +74,14 @@ export const FormPercentageField = React.forwardRef<
|
||||
throw percentageOrError.error;
|
||||
}
|
||||
|
||||
return (
|
||||
percentageOrError.object
|
||||
.toNumber()
|
||||
//.toPrecision(precision ?? value.precision)
|
||||
.toString()
|
||||
);
|
||||
return percentageOrError.object.toString();
|
||||
},
|
||||
output: (value: string | undefined) => {
|
||||
const percentageOrError = Percentage.create({
|
||||
amount: value?.replace(",", "") ?? null,
|
||||
precision,
|
||||
});
|
||||
output: (value: string | undefined, name?: string, values?: CurrencyInputOnChangeValues) => {
|
||||
console.log(values);
|
||||
const { value: amount } = values ?? { value: null };
|
||||
|
||||
const percentageOrError = Percentage.createFromFormattedValue(amount);
|
||||
|
||||
if (percentageOrError.isFailure) {
|
||||
throw percentageOrError.error;
|
||||
}
|
||||
@ -122,10 +118,16 @@ export const FormPercentageField = React.forwardRef<
|
||||
groupSeparator='.'
|
||||
decimalSeparator=','
|
||||
placeholder={placeholder}
|
||||
decimalsLimit={precision}
|
||||
decimalScale={precision}
|
||||
allowDecimals={scale !== 0}
|
||||
decimalsLimit={scale}
|
||||
decimalScale={scale}
|
||||
step={1}
|
||||
//{...field}
|
||||
value={transform.input(field.value)}
|
||||
onValueChange={(e) => field.onChange(transform.output(e))}
|
||||
//onChange={() => {}}
|
||||
onValueChange={(value, name, values) =>
|
||||
field.onChange(transform.output(value, name, values))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
{description && <FormDescription>{description}</FormDescription>}
|
||||
|
||||
@ -4,7 +4,7 @@ import { cn } from "@/lib/utils";
|
||||
import { FormControl, FormDescription, FormField, FormItem, InputProps } from "@/ui";
|
||||
import { Quantity } from "@shared/contexts";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import CurrencyInput from "react-currency-input-field";
|
||||
import CurrencyInput, { CurrencyInputOnChangeValues } from "react-currency-input-field";
|
||||
import { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form";
|
||||
import { FormErrorMessage } from "./FormErrorMessage";
|
||||
import { FormLabel, FormLabelProps } from "./FormLabel";
|
||||
@ -38,7 +38,7 @@ export type FormQuantityFieldProps<
|
||||
FormInputWithIconProps &
|
||||
UseControllerProps<TFieldValues, TName> &
|
||||
VariantProps<typeof formQuantityFieldVariants> & {
|
||||
precision: number;
|
||||
scale: number;
|
||||
};
|
||||
|
||||
export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantityFieldProps>(
|
||||
@ -54,7 +54,7 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
|
||||
defaultValue,
|
||||
rules,
|
||||
readOnly,
|
||||
precision,
|
||||
scale,
|
||||
variant,
|
||||
} = props;
|
||||
|
||||
@ -71,18 +71,12 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
|
||||
throw quantityOrError.error;
|
||||
}
|
||||
|
||||
return (
|
||||
quantityOrError.object
|
||||
.toNumber()
|
||||
//.toPrecision(precision ?? value.precision)
|
||||
.toString()
|
||||
);
|
||||
return quantityOrError.object.toString();
|
||||
},
|
||||
output: (value: string | undefined) => {
|
||||
const quantityOrError = Quantity.create({
|
||||
amount: value?.replace(",", "") ?? null,
|
||||
precision,
|
||||
});
|
||||
output: (value: string | undefined, name?: string, values?: CurrencyInputOnChangeValues) => {
|
||||
const { value: amount } = values ?? { value: null };
|
||||
|
||||
const quantityOrError = Quantity.createFromFormattedValue(amount);
|
||||
if (quantityOrError.isFailure) {
|
||||
throw quantityOrError.error;
|
||||
}
|
||||
@ -107,7 +101,7 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
|
||||
<FormControl>
|
||||
<CurrencyInput
|
||||
name={field.name}
|
||||
//ref={field.ref} <-- no activar que hace cosas raras
|
||||
//ref={field.ref} //<-- no activar que hace cosas raras
|
||||
onBlur={field.onBlur}
|
||||
disabled={field.disabled}
|
||||
readOnly={readOnly}
|
||||
@ -115,10 +109,16 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
|
||||
groupSeparator='.'
|
||||
decimalSeparator=','
|
||||
placeholder={placeholder}
|
||||
decimalsLimit={precision}
|
||||
decimalScale={precision}
|
||||
allowDecimals={scale !== 0}
|
||||
decimalsLimit={scale}
|
||||
decimalScale={scale}
|
||||
step={1}
|
||||
//{...field}
|
||||
value={transform.input(field.value)}
|
||||
onValueChange={(e) => field.onChange(transform.output(e))}
|
||||
//onChange={() => {}}
|
||||
onValueChange={(value, name, values) =>
|
||||
field.onChange(transform.output(value, name, values))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
{description && <FormDescription>{description}</FormDescription>}
|
||||
|
||||
@ -3,7 +3,6 @@ export * from "./FormDatePickerField";
|
||||
export * from "./FormErrorMessage";
|
||||
export * from "./FormGroup";
|
||||
export * from "./FormLabel";
|
||||
export * from "./FormMoneyField";
|
||||
export * from "./FormPercentageField";
|
||||
export * from "./FormQuantityField";
|
||||
export * from "./FormTextAreaField";
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
// jest.config.js
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsconfig: "tsconfig.json",
|
||||
},
|
||||
},
|
||||
moduleFileExtensions: ["ts", "js"],
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": "ts-jest",
|
||||
},
|
||||
testMatch: ["**/*.test.(ts|js)"],
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
moduleNameMapper: {
|
||||
"^@shared/(.*)$": "<rootDir>/../shared/lib/$1",
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
},
|
||||
};
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
"build": "tsc",
|
||||
"lint": "eslint --ignore-path .gitignore . --ext .ts",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"test": "jest --verbose",
|
||||
"test": "jest --config=./jest.config.ts --rootDir=./src/ --verbose",
|
||||
"clean": "rm -rf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -22,7 +22,7 @@
|
||||
"@types/express-session": "^1.18.0",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/http-status": "^1.1.2",
|
||||
"@types/jest": "^29.5.6",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/module-alias": "^2.0.1",
|
||||
@ -49,7 +49,7 @@
|
||||
"module-alias": "^2.2.3",
|
||||
"prettier": "3.0.1",
|
||||
"supertest": "^6.2.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-jest": "^29.2.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
import { IRepositoryManager } from "@/contexts/common/domain";
|
||||
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
|
||||
import { UniqueID } from "@shared/contexts";
|
||||
import { IQuoteRepository } from "../../domain";
|
||||
|
||||
import { Quote } from "../../domain/entities/Quotes/Quote";
|
||||
import { ISalesContext } from "../../infrastructure";
|
||||
import { GetQuoteUseCase } from "./GetQuote.useCase";
|
||||
|
||||
describe("GetQuoteUseCase", () => {
|
||||
let getQuoteUseCase: GetQuoteUseCase;
|
||||
let mockAdapter: jest.Mocked<ISequelizeAdapter>;
|
||||
let mockRepositoryManager: jest.Mocked<IRepositoryManager>;
|
||||
let mockQuoteRepository: jest.Mocked<IQuoteRepository>;
|
||||
let mockTransaction: { complete: jest.Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
mockTransaction = { complete: jest.fn() };
|
||||
|
||||
mockAdapter = {
|
||||
startTransaction: jest.fn().mockReturnValue(mockTransaction),
|
||||
} as unknown as jest.Mocked<ISequelizeAdapter>;
|
||||
|
||||
mockQuoteRepository = {
|
||||
getById: jest.fn(),
|
||||
} as unknown as jest.Mocked<IQuoteRepository>;
|
||||
|
||||
mockRepositoryManager = {
|
||||
getRepository: jest.fn().mockReturnValue(() => mockQuoteRepository),
|
||||
} as unknown as jest.Mocked<IRepositoryManager>;
|
||||
|
||||
const context: ISalesContext = {
|
||||
adapter: mockAdapter,
|
||||
repositoryManager: mockRepositoryManager,
|
||||
dealer: undefined,
|
||||
};
|
||||
|
||||
getQuoteUseCase = new GetQuoteUseCase(context);
|
||||
});
|
||||
|
||||
it("should return a quote when found", async () => {
|
||||
const id = new UniqueID();
|
||||
const quote = new Quote({ id, content: "Test Quote" });
|
||||
|
||||
mockQuoteRepository.getById.mockResolvedValueOnce(quote);
|
||||
mockTransaction.complete.mockImplementationOnce(async (fn: any) => await fn({}));
|
||||
|
||||
const request = { id };
|
||||
const response = await getQuoteUseCase.execute(request);
|
||||
|
||||
expect(response.isSuccess).toBe(true);
|
||||
expect(response.getValue()).toEqual(quote);
|
||||
});
|
||||
|
||||
it("should return a NOT_FOUND_ERROR when quote is not found", async () => {
|
||||
const id = new UniqueID();
|
||||
|
||||
mockQuoteRepository.getById.mockResolvedValueOnce(null);
|
||||
mockTransaction.complete.mockImplementationOnce(async (fn: any) => await fn({}));
|
||||
|
||||
const request = { id };
|
||||
const response = await getQuoteUseCase.execute(request);
|
||||
|
||||
expect(response.isFailure).toBe(true);
|
||||
expect(response.errorValue().message).toBe("Quote not found");
|
||||
});
|
||||
|
||||
it("should return a REPOSITORY_ERROR on database error", async () => {
|
||||
const id = new UniqueID();
|
||||
const error = new Error("Database error");
|
||||
|
||||
mockQuoteRepository.getById.mockRejectedValueOnce(error);
|
||||
mockTransaction.complete.mockImplementationOnce(async (fn: any) => await fn({}));
|
||||
|
||||
const request = { id };
|
||||
const response = await getQuoteUseCase.execute(request);
|
||||
|
||||
expect(response.isFailure).toBe(true);
|
||||
expect(response.errorValue().message).toBe("Query error");
|
||||
});
|
||||
});
|
||||
@ -37,9 +37,6 @@ export class GetQuoteUseCase
|
||||
async execute(request: IGetQuoteUseCaseRequest): Promise<GetQuoteResponseOrError> {
|
||||
const { id } = request;
|
||||
|
||||
// Validación de datos
|
||||
// No hay en este caso
|
||||
|
||||
return await this.findQuote(id);
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["jest"],
|
||||
"baseUrl": "./src",
|
||||
//"baseUrl": "./src",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@shared/*": ["../shared/lib/*"]
|
||||
|
||||
@ -25,10 +25,11 @@ describe("MoneyValue Value Object", () => {
|
||||
it("Should create a valid money value from string and format it", () => {
|
||||
const validMoneyValueFromString = MoneyValue.create({
|
||||
amount: "5075",
|
||||
scale: 2,
|
||||
});
|
||||
|
||||
expect(validMoneyValueFromString.isSuccess).toBe(true);
|
||||
expect(validMoneyValueFromString.object.toString()).toEqual("50,75 €");
|
||||
expect(validMoneyValueFromString.object.toString()).toEqual("50.75");
|
||||
});
|
||||
|
||||
// Prueba la creación de un valor monetario con una cadena no válida.
|
||||
@ -52,7 +53,7 @@ describe("MoneyValue Value Object", () => {
|
||||
|
||||
expect(moneyValue.getAmount()).toBe(100);
|
||||
expect(moneyValue.getCurrency().code).toBe("EUR");
|
||||
expect(moneyValue.getPrecision()).toBe(3);
|
||||
expect(moneyValue.getScale()).toBe(3);
|
||||
});
|
||||
|
||||
it("should create MoneyValue from string and currency", () => {
|
||||
@ -67,7 +68,7 @@ describe("MoneyValue Value Object", () => {
|
||||
|
||||
expect(moneyValue.getAmount()).toBe(12345);
|
||||
expect(moneyValue.getCurrency().code).toBe("USD");
|
||||
expect(moneyValue.getPrecision()).toBe(2);
|
||||
expect(moneyValue.getScale()).toBe(2);
|
||||
});
|
||||
|
||||
it("should fail to create MoneyValue with invalid amount", () => {
|
||||
@ -86,12 +87,7 @@ describe("MoneyValue Value Object", () => {
|
||||
}).object;
|
||||
const result = moneyValue.toString();
|
||||
|
||||
expect(result).toBe("7525");
|
||||
});
|
||||
|
||||
// Prueba la verificación de valor nulo.
|
||||
it("Should check if value is null", () => {
|
||||
expect(() => MoneyValue.create(null)).toThrowError();
|
||||
expect(result).toBe("75.25");
|
||||
});
|
||||
|
||||
// Prueba la verificación de valor cero.
|
||||
|
||||
@ -20,7 +20,7 @@ export const defaultMoneyValueOptions: IMoneyValueOptions = {
|
||||
};
|
||||
|
||||
export interface MoneyValueObject {
|
||||
amount: number;
|
||||
amount: number | null;
|
||||
scale: number;
|
||||
currency_code: string;
|
||||
}
|
||||
@ -43,7 +43,7 @@ export interface IMoneyValueProps {
|
||||
}
|
||||
|
||||
const defaultMoneyValueProps = {
|
||||
amount: 0,
|
||||
amount: null,
|
||||
currencyCode: CurrencyData.DEFAULT_CURRENCY_CODE,
|
||||
scale: 2,
|
||||
};
|
||||
@ -141,6 +141,8 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
scale = defaultMoneyValueProps.scale,
|
||||
} = props || {};
|
||||
|
||||
console.log(props, { amount, currencyCode, scale });
|
||||
|
||||
const validationResult = MoneyValue.validate(amount, options);
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
@ -159,6 +161,71 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
return Result.ok<MoneyValue>(new this(prop, isNull(_amount), options));
|
||||
}
|
||||
|
||||
public static createFromFormattedValue(
|
||||
value: NullOr<number | string>,
|
||||
currencyCode: string,
|
||||
_options: IMoneyValueOptions = {
|
||||
locale: defaultMoneyValueOptions.locale,
|
||||
}
|
||||
) {
|
||||
if (value === null || value === "") {
|
||||
return MoneyValue.create({
|
||||
amount: null,
|
||||
scale: MoneyValue.DEFAULT_SCALE,
|
||||
currencyCode,
|
||||
});
|
||||
}
|
||||
|
||||
const valueStr = String(value);
|
||||
const [integerPart, decimalPart] = valueStr.split(",");
|
||||
|
||||
let _amount = integerPart;
|
||||
let _scale = 2;
|
||||
|
||||
if (decimalPart === undefined) {
|
||||
// 99
|
||||
_scale = 0;
|
||||
} else {
|
||||
if (decimalPart === "") {
|
||||
// 99,
|
||||
_amount = integerPart + decimalPart.padEnd(1, "0");
|
||||
_scale = 1;
|
||||
}
|
||||
if (decimalPart.length === 1) {
|
||||
// 99,1
|
||||
_amount = integerPart + decimalPart.padEnd(1, "0");
|
||||
_scale = 1;
|
||||
} else {
|
||||
if (decimalPart.length === 2) {
|
||||
// 99,12
|
||||
_amount = integerPart + decimalPart.padEnd(2, "0");
|
||||
_scale = 2;
|
||||
} else {
|
||||
if (decimalPart.length === 3) {
|
||||
// 99,123
|
||||
_amount = integerPart + decimalPart.padEnd(3, "0");
|
||||
_scale = 3;
|
||||
} else {
|
||||
if (decimalPart.length === 4) {
|
||||
// 99,1235
|
||||
_amount = integerPart + decimalPart.padEnd(4, "0");
|
||||
_scale = 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MoneyValue.create(
|
||||
{
|
||||
amount: _amount,
|
||||
scale: _scale,
|
||||
currencyCode,
|
||||
},
|
||||
_options
|
||||
);
|
||||
}
|
||||
|
||||
private static sanitize(amount: NullOr<number | string>): NullOr<number> {
|
||||
let _amount: NullOr<number> = null;
|
||||
|
||||
@ -191,6 +258,16 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
.object;
|
||||
}
|
||||
|
||||
private static _toString(value: NullOr<number>, scale: number): string {
|
||||
if (value === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const factor = Math.pow(10, scale);
|
||||
const amount = Number(value) / factor;
|
||||
return amount.toFixed(scale);
|
||||
}
|
||||
|
||||
constructor(value: Dinero, isNull: boolean, options: IMoneyValueOptions) {
|
||||
super(value);
|
||||
this._isNull = Object.freeze(isNull);
|
||||
@ -202,7 +279,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
};
|
||||
|
||||
public toString(): string {
|
||||
return this._isNull ? "" : String(this.props?.getAmount());
|
||||
return MoneyValue._toString(this.isNull() ? null : this.getAmount(), this.getScale());
|
||||
}
|
||||
|
||||
public toJSON() {
|
||||
@ -320,7 +397,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
|
||||
public toObject(): MoneyValueObject {
|
||||
const obj = this.props.toObject();
|
||||
return {
|
||||
amount: obj.amount,
|
||||
amount: this._isNull ? null : obj.amount,
|
||||
scale: obj.precision,
|
||||
currency_code: String(obj.currency),
|
||||
};
|
||||
|
||||
@ -47,22 +47,33 @@ export class Percentage extends NullableValueObject<IPercentage> {
|
||||
scale: NullOr<number>,
|
||||
options: IPercentageOptions
|
||||
) {
|
||||
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED.default(
|
||||
defaultPercentageProps.amount
|
||||
const ruleNull = RuleValidator.RULE_ALLOW_NULL_OR_UNDEFINED;
|
||||
|
||||
const ruleEmpty = RuleValidator.RULE_ALLOW_EMPTY;
|
||||
|
||||
const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label(
|
||||
options.label ? options.label : "amount"
|
||||
);
|
||||
|
||||
const ruleString = RuleValidator.RULE_IS_TYPE_STRING.regex(/^[-]?\d+$/).label(
|
||||
options.label ? options.label : "amount"
|
||||
);
|
||||
|
||||
const ruleScale = Joi.number()
|
||||
.min(Percentage.MIN_SCALE)
|
||||
.max(Percentage.MAX_SCALE)
|
||||
.label(options.label ? options.label : "percentage");
|
||||
.label(options.label ? options.label : "scale");
|
||||
|
||||
const validationResults = new ResultCollection([
|
||||
RuleValidator.validate<NullOr<number>>(
|
||||
Joi.alternatives(ruleNull, RuleValidator.RULE_IS_TYPE_NUMBER),
|
||||
Joi.alternatives(ruleNull, ruleEmpty, ruleNumber, ruleString),
|
||||
value
|
||||
),
|
||||
RuleValidator.validate<NullOr<number>>(
|
||||
Joi.alternatives(ruleNull, RuleValidator.RULE_IS_TYPE_NUMBER, ruleScale),
|
||||
Joi.alternatives(
|
||||
RuleValidator.RULE_IS_TYPE_NUMBER.label(options.label ? options.label : "scale"),
|
||||
ruleScale
|
||||
),
|
||||
scale
|
||||
),
|
||||
]);
|
||||
@ -181,18 +192,18 @@ export class Percentage extends NullableValueObject<IPercentage> {
|
||||
return _value;
|
||||
}
|
||||
|
||||
private static _toNumber(value: NullOr<number>, scale: number): number {
|
||||
if (isNull(value)) {
|
||||
return 0;
|
||||
private static _toString(value: NullOr<number>, scale: number): string {
|
||||
if (value === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const factor = Math.pow(10, scale);
|
||||
const amount = Number(value) / factor;
|
||||
return Number(amount.toFixed(scale));
|
||||
return amount.toFixed(scale);
|
||||
}
|
||||
|
||||
private static _isWithinRange(value: NullOr<number>, scale: number): boolean {
|
||||
const _value = Percentage._toNumber(value, scale);
|
||||
const _value = Number(Percentage._toString(value, scale));
|
||||
return _value >= Percentage.MIN_VALUE && _value <= Percentage.MAX_VALUE;
|
||||
}
|
||||
|
||||
@ -226,12 +237,12 @@ export class Percentage extends NullableValueObject<IPercentage> {
|
||||
return this._isNull;
|
||||
};
|
||||
|
||||
public toNumber(): number {
|
||||
return Percentage._toNumber(this.amount, this.scale);
|
||||
public toString(): string {
|
||||
return Percentage._toString(this.amount, this.scale);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.isNull() ? "" : String(this.toNumber());
|
||||
public toNumber(): number {
|
||||
return this.isNull() ? 0 : Number(this.toString());
|
||||
}
|
||||
|
||||
public toPrimitive(): NullOr<number> {
|
||||
|
||||
@ -47,27 +47,28 @@ export class Quantity extends NullableValueObject<IQuantity> {
|
||||
const ruleEmpty = RuleValidator.RULE_ALLOW_EMPTY;
|
||||
|
||||
const ruleNumber = RuleValidator.RULE_IS_TYPE_NUMBER.label(
|
||||
options.label ? options.label : "quantity"
|
||||
options.label ? options.label : "amount"
|
||||
);
|
||||
|
||||
const ruleString = RuleValidator.RULE_IS_TYPE_STRING.regex(/^[-]?\d+$/).label(
|
||||
options.label ? options.label : "quantity"
|
||||
options.label ? options.label : "amount"
|
||||
);
|
||||
|
||||
const ruleScale = Joi.number()
|
||||
.min(Quantity.MIN_SCALE)
|
||||
.max(Quantity.MAX_SCALE)
|
||||
.label(options.label ? options.label : "quantity");
|
||||
|
||||
const rules = Joi.alternatives(ruleNull, ruleEmpty, ruleNumber, ruleString);
|
||||
.label(options.label ? options.label : "scale");
|
||||
|
||||
const validationResults = new ResultCollection([
|
||||
RuleValidator.validate<NullOr<number>>(
|
||||
Joi.alternatives(ruleNull, ruleNumber, ruleString),
|
||||
Joi.alternatives(ruleNull, ruleEmpty, ruleNumber, ruleString),
|
||||
value
|
||||
),
|
||||
RuleValidator.validate<NullOr<number>>(
|
||||
Joi.alternatives(ruleNull, RuleValidator.RULE_IS_TYPE_NUMBER, ruleScale),
|
||||
Joi.alternatives(
|
||||
RuleValidator.RULE_IS_TYPE_NUMBER.label(options.label ? options.label : "scale"),
|
||||
ruleScale
|
||||
),
|
||||
scale
|
||||
),
|
||||
]);
|
||||
@ -76,15 +77,6 @@ export class Quantity extends NullableValueObject<IQuantity> {
|
||||
return validationResults.getFirstFaultyResult();
|
||||
}
|
||||
|
||||
// Convert the value to a number if it's a string
|
||||
let numericValue = typeof value === "string" ? parseInt(value, 10) : Number(value);
|
||||
|
||||
// Check if scale is null, and set to default if so
|
||||
let numericScale = scale === null ? Quantity.DEFAULT_SCALE : Number(scale);
|
||||
|
||||
// Calculate the adjusted value
|
||||
const adjustedValue = numericValue / Math.pow(10, numericScale);
|
||||
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user