This commit is contained in:
David Arranz 2025-04-14 21:12:16 +02:00
parent a20756724c
commit 340cd11311
26 changed files with 1829 additions and 1294 deletions

View File

@ -1,2 +1,2 @@
VITE_API_URL=http://192.168.0.130:4001/api/v1 VITE_API_URL=http://192.168.0.116:4001/api/v1
VITE_API_KEY=e175f809ba71fb2765ad5e60f9d77596-es19 VITE_API_KEY=e175f809ba71fb2765ad5e60f9d77596-es19

View File

@ -5,11 +5,12 @@
"author": "Rodax Software <dev@rodax-software.com>", "author": "Rodax Software <dev@rodax-software.com>",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host --debug",
"build": "rm -rf ../dist/client && tsc && vite build", "build": "rm -rf ../dist/client && tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview --host --port 8080", "preview": "vite preview --host --port 8080",
"test": "jest" "test": "jest",
"clean": "rm -rf node_modules"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.1.0", "@dnd-kit/core": "^6.1.0",
@ -54,15 +55,15 @@
"i18next": "^23.12.2", "i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"install": "^0.13.0", "install": "^0.13.0",
"joi": "^17.13.1", "joi": "^17.13.3",
"lucide-react": "^0.427.0", "lucide-react": "^0.427.0",
"print-js": "^1.6.0", "print-js": "^1.6.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-currency-input-field": "^3.8.0", "react-currency-input-field": "^3.10.0",
"react-day-picker": "^8.10.1", "react-day-picker": "^8.10.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.52.2", "react-hook-form": "^7.55.0",
"react-hook-form-persist": "^3.0.0", "react-hook-form-persist": "^3.0.0",
"react-i18next": "^15.0.1", "react-i18next": "^15.0.1",
"react-pdf": "^9.1.0", "react-pdf": "^9.1.0",
@ -73,10 +74,12 @@
"recharts": "^2.12.7", "recharts": "^2.12.7",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.5.4",
"use-debounce": "^10.0.3", "use-debounce": "^10.0.3",
"vaul": "^0.9.1" "vaul": "^0.9.1"
}, },
"devDependencies": { "devDependencies": {
"@hookform/devtools": "^4.4.0",
"@tanstack/react-query-devtools": "^5.51.23", "@tanstack/react-query-devtools": "^5.51.23",
"@testing-library/jest-dom": "^6.4.8", "@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0", "@testing-library/react": "^16.0.0",
@ -103,8 +106,7 @@
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.13",
"ts-jest": "^29.2.4", "ts-jest": "^29.2.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.5.4", "vite": "^5.4.16",
"vite": "^5.4.0",
"vite-plugin-robots": "^1.0.5", "vite-plugin-robots": "^1.0.5",
"vite-plugin-static-copy": "^1.0.6" "vite-plugin-static-copy": "^1.0.6"
} }

View File

@ -17,18 +17,6 @@ import { QuotesList } from "./app/quotes/list";
import { ProtectedRoute } from "./components"; import { ProtectedRoute } from "./components";
export const Routes = () => { export const Routes = () => {
// Define public routes accessible to all users
const routesForPublic = [
{
path: "/",
element: (
<ProtectedRoute>
<Navigate to='/quotes' replace={true} />
</ProtectedRoute>
),
},
];
const routesForErrors = [ const routesForErrors = [
{ {
path: "*", path: "*",
@ -38,6 +26,14 @@ export const Routes = () => {
// Define routes accessible only to authenticated users // Define routes accessible only to authenticated users
const routesForAuthenticatedOnly = [ const routesForAuthenticatedOnly = [
{
path: "/",
element: (
<ProtectedRoute>
<Navigate to='/quotes' replace={true} />
</ProtectedRoute>
),
},
{ {
path: "/home", path: "/home",
element: ( element: (
@ -126,12 +122,7 @@ export const Routes = () => {
// Combine and conditionally include routes based on authentication status // Combine and conditionally include routes based on authentication status
const router = createBrowserRouter( const router = createBrowserRouter(
[ [...routesForAuthenticatedOnly, ...routesForNotAuthenticatedOnly, ...routesForErrors],
...routesForPublic,
...routesForAuthenticatedOnly,
...routesForNotAuthenticatedOnly,
...routesForErrors,
],
{ {
//basename: "/app", //basename: "/app",
} }

View File

@ -62,6 +62,7 @@ export const LoginPageWithLanguageSelector = () => {
const { mutate: login } = useLogin({ const { mutate: login } = useLogin({
onSuccess: (data) => { onSuccess: (data) => {
console.debug("data", data);
const { success, error } = data; const { success, error } = data;
if (!success && error) { if (!success && error) {
form.setError("root", error); form.setError("root", error);

View File

@ -1,25 +1,84 @@
import { FormPercentageField } from "@/components"; import { FormPercentageField } from "@/components";
import { useLocalization } from "@/lib/hooks"; import { useLocalization } from "@/lib/hooks";
import { Card, CardContent, CardDescription, CardTitle, Separator } from "@/ui"; import { Card, CardContent, CardDescription, CardTitle, Separator } from "@/ui";
import { CurrencyData } from "@shared/contexts"; import { CurrencyData, MoneyValue, Percentage } from "@shared/contexts";
import { t } from "i18next"; import { t } from "i18next";
import { useMemo } from "react"; import { useMemo } from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext, useWatch } from "react-hook-form";
export const QuotePricesResume = () => { const calculateQuoteTotals = (prices: any) => {
const { watch, register, formState } = useFormContext(); console.debug("calculateQuoteTotals", prices);
const discountOrError = Percentage.create(
prices[1] || {
amount: null,
scale: 2,
}
);
if (discountOrError.isFailure) {
throw discountOrError.error;
}
const discount = discountOrError.object;
const taxOrError = Percentage.create(
prices[2] || {
amount: null,
scale: 2,
}
);
if (taxOrError.isFailure) {
throw taxOrError.error;
}
const tax = taxOrError.object;
const subtotalOrError = MoneyValue.create(
prices[0] || {
amount: null,
scale: 2,
}
);
if (subtotalOrError.isFailure) {
throw subtotalOrError.error;
}
const subtotalPrice = subtotalOrError.object;
const discountPrice = subtotalPrice.percentage(discount.toNumber()).convertScale(2);
const priceBeforeTaxes = subtotalPrice.subtract(discountPrice).convertScale(2);
const taxesPrice = priceBeforeTaxes.percentage(tax.toNumber()).convertScale(2);
const totalPrice = priceBeforeTaxes.add(taxesPrice).convertScale(2);
return {
subtotalPrice: subtotalPrice.toObject(),
discount: discount.toObject(),
discountPrice: discountPrice.toObject(),
priceBeforeTaxes: priceBeforeTaxes.toObject(),
tax: tax.toObject(),
taxesPrice: taxesPrice.toObject(),
totalPrice: totalPrice.toObject(),
};
};
export const QuotePricesResume = ({ currency }: { currency: CurrencyData }) => {
const { register, formState, control } = useFormContext();
const { formatNumber } = useLocalization(); const { formatNumber } = useLocalization();
const currency_code = watch("currency_code"); const pricesWatch = useWatch({ control, name: ["subtotal_price", "discount", "tax"] });
const subtotal_price = formatNumber(watch("subtotal_price"));
const discount_price = formatNumber(watch("discount_price"));
const tax_price = formatNumber(watch("tax_price"));
const total_price = formatNumber(watch("total_price"));
const currency_symbol = useMemo(() => { const totals = calculateQuoteTotals(pricesWatch);
const currencyOrError = CurrencyData.createFromCode(currency_code);
return currencyOrError.isSuccess ? currencyOrError.object.symbol : ""; const subtotal_price = formatNumber(totals.subtotalPrice);
}, [currency_code]); const discount_price = formatNumber(totals.discountPrice);
const tax_price = formatNumber(totals.taxesPrice);
const total_price = formatNumber(totals.totalPrice);
const currency_symbol = useMemo(() => currency.symbol || "", [currency]);
return ( return (
<Card className='w-full bg-muted'> <Card className='w-full bg-muted'>
@ -31,6 +90,7 @@ export const QuotePricesResume = () => {
</CardDescription> </CardDescription>
<CardTitle className='flex items-baseline justify-end text-2xl tabular-nums'> <CardTitle className='flex items-baseline justify-end text-2xl tabular-nums'>
{subtotal_price} {subtotal_price}
<span className='ml-1 text-lg tracking-normal'>{currency_symbol}</span> <span className='ml-1 text-lg tracking-normal'>{currency_symbol}</span>
</CardTitle> </CardTitle>
</div> </div>
@ -48,6 +108,9 @@ export const QuotePricesResume = () => {
{...register("discount", { {...register("discount", {
required: false, required: false,
})} })}
onChange={(value) => {
console.log("discount", value);
}}
/> />
</div> </div>
<div className='grid gap-1 font-semibold text-muted-foreground'> <div className='grid gap-1 font-semibold text-muted-foreground'>

View File

@ -31,7 +31,13 @@ export const QuoteDetailsCardEditor = ({
defaultValues: Readonly<{ [x: string]: any }> | undefined; defaultValues: Readonly<{ [x: string]: any }> | undefined;
}) => { }) => {
const { toast } = useToast(); const { toast } = useToast();
const { control, register } = useFormContext(); const {
control,
register,
watch,
setValue,
formState: { isDirty },
} = useFormContext();
const [pickerMode] = useState<"dialog" | "panel">("dialog"); const [pickerMode] = useState<"dialog" | "panel">("dialog");
@ -43,6 +49,75 @@ export const QuoteDetailsCardEditor = ({
name: "items", name: "items",
}); });
//const pricesWatch = useWatch({ control, name: "items" });
/* useEffect(() => {
if (!isDirty) {
return;
}
const subscription = watch((formData, { name, type }) => {
console.log(type, name);
if (name === "items") {
console.log("nueva fila agregada o fila eliminada o intercambiada");
} else if (type === "change" && name?.startsWith("items")) {
const index = Number(name.split(".")[1]);
const fieldName = name.split(".")[2];
if (["quantity", "unit_price", "discount"].includes(fieldName)) {
console.log(fieldName);
const item = formData.items[index];
const newPrices = calculateQuoteItemTotals(item);
console.log(newPrices.unit_price.toObject());
if (!isEqual(newPrices.quantity.toObject(), item.quantity)) {
console.log("quantity changed");
setValue(`items.${index}.quantity`, newPrices.quantity.toObject());
}
if (!isEqual(newPrices.unit_price.toObject(), item.unit_price)) {
console.log("unit_price changed");
setValue(`items.${index}.unit_price`, newPrices.unit_price.toObject());
}
if (!isEqual(newPrices.discount.toObject(), item.discount)) {
console.log("discount changed");
setValue(`items.${index}.discount`, newPrices.discount.toObject());
}
if (!isEqual(newPrices.subtotal_price.toObject(), item.subtotal_price)) {
console.log("subtotal_price changed");
setValue(`items.${index}.subtotal_price`, newPrices.subtotal_price.toObject());
}
if (!isEqual(newPrices.total_price.toObject(), item.total_price)) {
console.log("total_price changed");
setValue(`items.${index}.total_price`, newPrices.total_price.toObject());
}
}
}
});
return () => subscription.unsubscribe();
}, [watch, isDirty, setValue]); */
/*const items = oldItems.map((item: any) => {
const newPrices = calculateQuoteItemTotals(item);
return {
...item,
quantity: newPrices.quantity,
unit_price: newPrices.unitPrice,
discount: newPrices.discount,
subtotal_price: newPrices.subtotalPrice,
total_price: newPrices.totalPrice,
};
});*/
//console.log(pricesWatch);
//fieldActions.replace(items);
const columns: ColumnDef<RowIdData, unknown>[] = useDetailColumns( const columns: ColumnDef<RowIdData, unknown>[] = useDetailColumns(
[ [
/*{ /*{
@ -84,7 +159,8 @@ export const QuoteDetailsCardEditor = ({
/> />
); );
}, },
size: 500, minSize: 200,
size: 400,
}, },
{ {
id: "quantity" as const, id: "quantity" as const,
@ -101,6 +177,7 @@ export const QuoteDetailsCardEditor = ({
/> />
); );
}, },
size: 75,
}, },
{ {
id: "unit_price" as const, id: "unit_price" as const,
@ -119,6 +196,7 @@ export const QuoteDetailsCardEditor = ({
/> />
); );
}, },
size: 125,
}, },
{ {
id: "subtotal_price" as const, id: "subtotal_price" as const,
@ -129,6 +207,7 @@ export const QuoteDetailsCardEditor = ({
cell: ({ row: { index } }) => { cell: ({ row: { index } }) => {
return ( return (
<FormCurrencyField <FormCurrencyField
variant='ghost'
currency={currency} currency={currency}
language={language} language={language}
scale={2} scale={2}
@ -138,6 +217,7 @@ export const QuoteDetailsCardEditor = ({
/> />
); );
}, },
size: 150,
}, },
{ {
id: "discount" as const, id: "discount" as const,
@ -154,6 +234,7 @@ export const QuoteDetailsCardEditor = ({
/> />
); );
}, },
size: 100,
}, },
{ {
id: "total_price" as const, id: "total_price" as const,
@ -174,6 +255,7 @@ export const QuoteDetailsCardEditor = ({
/> />
); );
}, },
size: 150,
}, },
], ],
{ {

View File

@ -118,7 +118,7 @@ export const QuoteCreate = () => {
description={t("quotes.create.form_groups.general.desc")} description={t("quotes.create.form_groups.general.desc")}
footerActions={ footerActions={
<div className='flex items-stretch justify-between flex-1'> <div className='flex items-stretch justify-between flex-1'>
<Button size='sm' variant={"ghost"} onClick={() => navigate("/quotes")}> <Button size='sm' variant={"outline"} onClick={() => navigate("/quotes")}>
{t("common.discard")} {t("common.discard")}
</Button> </Button>
<SubmitButton size='sm' label={t("common.continue")}></SubmitButton> <SubmitButton size='sm' label={t("common.continue")}></SubmitButton>

View File

@ -9,8 +9,9 @@ import {
import { calculateQuoteItemTotals, calculateQuoteTotals } from "@/lib/calc"; import { calculateQuoteItemTotals, calculateQuoteTotals } from "@/lib/calc";
import { useUnsavedChangesNotifier } from "@/lib/hooks"; import { useUnsavedChangesNotifier } from "@/lib/hooks";
import { useUrlId } from "@/lib/hooks/useUrlId"; import { useUrlId } from "@/lib/hooks/useUrlId";
import { Button, Form, Tabs, TabsContent, TabsList, TabsTrigger } from "@/ui"; import { Button, Form } from "@/ui";
import { useToast } from "@/ui/use-toast"; import { useToast } from "@/ui/use-toast";
import { DevTool } from "@hookform/devtools";
import { import {
CurrencyData, CurrencyData,
IGetQuote_QuoteItem_Response_DTO, IGetQuote_QuoteItem_Response_DTO,
@ -18,7 +19,8 @@ import {
Language, Language,
} from "@shared/contexts"; } from "@shared/contexts";
import { t } from "i18next"; import { t } from "i18next";
import { useEffect, useMemo, useState } from "react"; import { isEqual } from "lodash";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { QuotePricesResume } from "./components"; import { QuotePricesResume } from "./components";
@ -34,17 +36,21 @@ export const QuoteEdit = () => {
const quoteId = useUrlId(); const quoteId = useUrlId();
const { toast } = useToast(); const { toast } = useToast();
const [activeTab, setActiveTab] = useState("general");
const [quoteCurrency, setQuoteCurrency] = useState<CurrencyData>( const [quoteCurrency, setQuoteCurrency] = useState<CurrencyData>(
CurrencyData.createDefaultCode().object CurrencyData.createDefaultCode().object
); );
const [quoteLanguage, setQuoteLanguage] = useState<Language>(Language.createDefaultCode().object); const [quoteLanguage, setQuoteLanguage] = useState<Language>(Language.createDefaultCode().object);
const { useOne, useUpdate } = useQuotes(); // Los defaultValues se usan para inicializar el formulario, pero no se deben usar para resetear el formulario.
// Si se quiere resetear el formulario, se debe usar el método reset() de react-hook-form.
const { data, status, error: queryError } = useOne(quoteId); // No se debe usar ni 'undefined' ni 'null' como valor por defecto, ya que esto puede causar problemas con los validadores de react-hook-form.
// En su lugar, se deben usar valores por defecto válidos para cada campo.
// Por ejemplo, para un campo de texto, se puede usar una cadena vacía como valor por defecto.
// Para un campo numérico, se puede usar 0 o NaN como valor por defecto.
// Para un campo booleano, se puede usar false como valor por defecto.
// Para un campo de fecha, se puede usar una fecha válida como valor por defecto.
// Para un campo de selección, se puede usar el primer valor de la lista como valor por defecto.
// Para un campo de selección múltiple, se puede usar una lista vacía como valor por defecto.
const defaultValues = useMemo( const defaultValues = useMemo(
() => ({ () => ({
date: "", date: "",
@ -59,21 +65,21 @@ export const QuoteEdit = () => {
subtotal_price: { subtotal_price: {
amount: undefined, amount: undefined,
scale: 2, scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code, currency_code: "",
}, },
discount: { discount: {
amount: undefined, amount: 0,
scale: 0, scale: 0,
}, },
discount_price: { discount_price: {
amount: undefined, amount: 0,
scale: 2, scale: 0,
currency_code: data?.currency_code ?? quoteCurrency.code, currency_code: "",
}, },
before_tax_price: { before_tax_price: {
amount: undefined, amount: undefined,
scale: 2, scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code, currency_code: "",
}, },
tax: { tax: {
amount: undefined, amount: undefined,
@ -82,62 +88,232 @@ export const QuoteEdit = () => {
tax_price: { tax_price: {
amount: undefined, amount: undefined,
scale: 2, scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code, currency_code: "",
}, },
total_price: { total_price: {
amount: undefined, amount: undefined,
scale: 2, scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code, currency_code: "",
}, },
items: [ items: [
{ {
id_article: "", id_article: "",
description: "", description: "",
quantity: { quantity: {
amount: null, amount: undefined,
scale: 2, scale: 2,
}, },
unit_price: { unit_price: {
amount: null, amount: undefined,
scale: 2, scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code, currency_code: "",
}, },
subtotal_price: { subtotal_price: {
amount: null, amount: undefined,
scale: 2, scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code, currency_code: "",
}, },
discount: { discount: {
amount: null, amount: undefined,
scale: 2, scale: 2,
}, },
total_price: { total_price: {
amount: null, amount: undefined,
scale: 2, scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code, currency_code: "",
}, },
}, },
], ],
}), }),
[data, quoteCurrency] []
); );
const { useOne, useUpdate } = useQuotes();
const { data, status, error: queryError } = useOne(quoteId);
const { mutate, isPending } = useUpdate(String(quoteId)); const { mutate, isPending } = useUpdate(String(quoteId));
const form = useForm<QuoteDataForm>({ const form = useForm<QuoteDataForm>({
mode: "onBlur", mode: "onBlur",
values: data, defaultValues, // lo ideal es usar solo defaultValues y usar reset(data) cuando llegue la data del useOne.
defaultValues, //values: data,
//shouldUnregister: true, //shouldUnregister: true, // si hay muchos inputs dinámicos para optimizar performance y limpieza de campos en formularios grandes.
}); });
const { getValues, reset, handleSubmit, formState, watch, setValue } = form; const { getValues, reset, handleSubmit, formState, control, watch, setValue } = form;
const { isSubmitting, isDirty } = formState; const { isSubmitting, isDirty } = formState;
useUnsavedChangesNotifier({ useUnsavedChangesNotifier({
isDirty, isDirty,
}); });
useEffect(() => {
if (!isDirty) {
return;
}
const { unsubscribe } = watch((formData, { name, type }) => {
console.log("watch", name, type);
if (name === "items") {
console.log("nueva fila agregada o fila eliminada o intercambiada");
// Recalcular los totales de cada item
formData.items &&
formData.items.map((item, index) => {
if (item) {
const quoteItemTotals = calculateQuoteItemTotals(item);
if (!isEqual(quoteItemTotals.subtotal_price.toObject(), item.subtotal_price)) {
setValue(
`items.${index}.subtotal_price`,
quoteItemTotals.subtotal_price.toObject()
);
}
if (!isEqual(quoteItemTotals.total_price.toObject(), item.total_price)) {
setValue(`items.${index}.total_price`, quoteItemTotals.total_price.toObject());
}
}
});
// Recalcular los totales de la cotización
const quoteTotals = calculateQuoteTotals(formData, true);
if (!isEqual(quoteTotals.subtotal_price.toObject(), formData.total_price)) {
setValue("subtotal_price", quoteTotals.subtotal_price.toObject());
}
if (!isEqual(quoteTotals.discount_price.toObject(), formData.total_price)) {
setValue("discount_price", quoteTotals.discount_price.toObject());
}
if (!isEqual(quoteTotals.before_tax_price.toObject(), formData.before_tax_price)) {
setValue("before_tax_price", quoteTotals.before_tax_price.toObject());
}
if (!isEqual(quoteTotals.tax_price.toObject(), formData.tax_price)) {
setValue("tax_price", quoteTotals.tax_price.toObject());
}
if (!isEqual(quoteTotals.total_price.toObject(), formData.total_price)) {
setValue("total_price", quoteTotals.total_price.toObject());
}
} else if (name && type === "change") {
if (name === "currency_code") {
const currency = CurrencyData.createFromCode(
formData.currency_code ?? CurrencyData.DEFAULT_CURRENCY_CODE
);
if (currency.isFailure) {
console.error(currency.error);
throw currency.error;
}
setQuoteCurrency(currency.object);
}
if (name === "lang_code") {
const language = Language.createFromCode(
formData.lang_code ?? Language.DEFAULT_LANGUAGE_CODE
);
if (language.isFailure) {
console.error(language.error);
throw language.error;
}
setQuoteLanguage(language.object);
}
if (["discount", "tax"].includes(name)) {
// Recalcular los totales de la cotización
const quoteTotals = calculateQuoteTotals(formData, true);
if (!isEqual(quoteTotals.subtotal_price.toObject(), formData.total_price)) {
setValue("subtotal_price", quoteTotals.subtotal_price.toObject());
}
if (!isEqual(quoteTotals.discount_price.toObject(), formData.total_price)) {
setValue("discount_price", quoteTotals.discount_price.toObject());
}
if (!isEqual(quoteTotals.before_tax_price.toObject(), formData.before_tax_price)) {
setValue("before_tax_price", quoteTotals.before_tax_price.toObject());
}
if (!isEqual(quoteTotals.tax_price.toObject(), formData.tax_price)) {
setValue("tax_price", quoteTotals.tax_price.toObject());
}
if (!isEqual(quoteTotals.total_price.toObject(), formData.total_price)) {
setValue("total_price", quoteTotals.total_price.toObject());
}
}
if (name?.startsWith("items")) {
const index = Number(name.split(".")[1]);
const fieldName = name.split(".")[2];
if (["quantity", "unit_price", "discount"].includes(fieldName)) {
if (formData.items && formData.items[index]) {
const item = formData.items[index];
const newPrices = calculateQuoteItemTotals(item);
console.log(newPrices.unit_price.toObject());
// Recalcular los total de ese item
if (!isEqual(newPrices.quantity.toObject(), item.quantity)) {
console.log("quantity changed");
setValue(`items.${index}.quantity`, newPrices.quantity.toObject());
}
if (!isEqual(newPrices.unit_price.toObject(), item.unit_price)) {
console.log("unit_price changed");
setValue(`items.${index}.unit_price`, newPrices.unit_price.toObject());
}
if (!isEqual(newPrices.discount.toObject(), item.discount)) {
console.log("discount changed");
setValue(`items.${index}.discount`, newPrices.discount.toObject());
}
if (!isEqual(newPrices.subtotal_price.toObject(), item.subtotal_price)) {
console.log("subtotal_price changed");
setValue(`items.${index}.subtotal_price`, newPrices.subtotal_price.toObject());
}
if (!isEqual(newPrices.total_price.toObject(), item.total_price)) {
console.log("total_price changed");
setValue(`items.${index}.total_price`, newPrices.total_price.toObject());
}
// Recalcular los totales de la cotización
const quoteTotals = calculateQuoteTotals(formData, true);
if (!isEqual(quoteTotals.subtotal_price.toObject(), formData.total_price)) {
setValue("subtotal_price", quoteTotals.subtotal_price.toObject());
}
if (!isEqual(quoteTotals.discount_price.toObject(), formData.total_price)) {
setValue("discount_price", quoteTotals.discount_price.toObject());
}
if (!isEqual(quoteTotals.before_tax_price.toObject(), formData.before_tax_price)) {
setValue("before_tax_price", quoteTotals.before_tax_price.toObject());
}
if (!isEqual(quoteTotals.tax_price.toObject(), formData.tax_price)) {
setValue("tax_price", quoteTotals.tax_price.toObject());
}
if (!isEqual(quoteTotals.total_price.toObject(), formData.total_price)) {
setValue("total_price", quoteTotals.total_price.toObject());
}
}
}
}
}
});
return () => unsubscribe();
}, [watch, isDirty, setValue]);
const onSubmit = async (data: QuoteDataForm, shouldRedirect: boolean) => { const onSubmit = async (data: QuoteDataForm, shouldRedirect: boolean) => {
// Transformación del form -> typo de request // Transformación del form -> typo de request
@ -159,94 +335,20 @@ export const QuoteEdit = () => {
}); });
}; };
useEffect(() => { const handleClose = useCallback(() => {
const { unsubscribe } = watch((_, { name }) => {
const quote = getValues();
if (name) {
switch (true) {
case name === "currency_code":
setQuoteCurrency(
CurrencyData.createFromCode(quote.currency_code ?? CurrencyData.DEFAULT_CURRENCY_CODE)
.object
);
break;
case name === "lang_code":
setQuoteLanguage(
Language.createFromCode(quote.lang_code ?? Language.DEFAULT_LANGUAGE_CODE).object
);
break;
case name === "discount" || name === "tax": {
const quoteTotals = calculateQuoteTotals(quote);
setValue("subtotal_price", quoteTotals.subtotalPrice.toObject());
setValue("discount_price", quoteTotals.discountPrice.toObject());
setValue("before_tax_price", quoteTotals.priceBeforeTaxes.toObject());
setValue("tax_price", quoteTotals.taxesPrice.toObject());
setValue("total_price", quoteTotals.totalPrice.toObject());
break;
}
case name === "items": {
quote.items &&
quote.items.map((item, index) => {
const quoteItemTotals = calculateQuoteItemTotals(item);
setValue(`items.${index}.subtotal_price`, quoteItemTotals.subtotalPrice.toObject());
setValue(`items.${index}.total_price`, quoteItemTotals.totalPrice.toObject());
});
const quoteTotals = calculateQuoteTotals(quote, true);
setValue("subtotal_price", quoteTotals.subtotalPrice.toObject());
setValue("discount_price", quoteTotals.discountPrice.toObject());
setValue("before_tax_price", quoteTotals.priceBeforeTaxes.toObject());
setValue("tax_price", quoteTotals.taxesPrice.toObject());
setValue("total_price", quoteTotals.totalPrice.toObject());
break;
}
case name.endsWith("quantity") ||
name.endsWith("unit_price") ||
name.endsWith("discount"): {
const [, indexString] = String(name).split(".");
const index = parseInt(indexString);
const quoteItemTotals = calculateQuoteItemTotals(quote.items[index]);
setValue(`items.${index}.subtotal_price`, quoteItemTotals.subtotalPrice.toObject());
setValue(`items.${index}.total_price`, quoteItemTotals.totalPrice.toObject());
// Cabecera
const quoteTotals = calculateQuoteTotals(quote, true);
setValue("subtotal_price", quoteTotals.subtotalPrice.toObject());
setValue("discount_price", quoteTotals.discountPrice.toObject());
setValue("before_tax_price", quoteTotals.priceBeforeTaxes.toObject());
setValue("tax_price", quoteTotals.taxesPrice.toObject());
setValue("total_price", quoteTotals.totalPrice.toObject());
break;
}
default:
break;
}
}
});
return () => unsubscribe();
}, [watch, getValues, setValue]);
const handleClose = () => {
navigate("/quotes", { navigate("/quotes", {
state: { state: {
id: quoteId, id: quoteId,
}, },
}); });
}; }, [navigate, quoteId]);
if (isSubmitting || isPending) { // Reset form when data is fetched
//return <LoadingOverlay title='Guardando cotización' />; useEffect(() => {
} if (status === "success" && data) {
reset(data);
}
}, [status, data, reset]);
if (status === "error") { if (status === "error") {
return <ErrorOverlay errorMessage={queryError.message} />; return <ErrorOverlay errorMessage={queryError.message} />;
@ -294,42 +396,25 @@ export const QuoteEdit = () => {
</div> </div>
<QuoteGeneralCardEditor /> <QuoteGeneralCardEditor />
<QuotePricesResume /> <QuotePricesResume currency={quoteCurrency} />
<QuoteDetailsCardEditor <QuoteDetailsCardEditor
currency={quoteCurrency} currency={quoteCurrency}
language={quoteLanguage} language={quoteLanguage}
defaultValues={defaultValues} defaultValues={defaultValues}
/> />
<Tabs
defaultValue='items'
className='hidden space-y-4 '
value={activeTab}
onValueChange={setActiveTab}
>
<TabsList>
<TabsTrigger value='general'>{t("quotes.create.tabs.general")}</TabsTrigger>
<TabsTrigger value='items'>{t("quotes.create.tabs.items")}</TabsTrigger>
{/* <TabsTrigger value='history'>{t("quotes.create.tabs.history")}</TabsTrigger>*/}
</TabsList>
<TabsContent
value='general'
forceMount
hidden={"general" !== activeTab}
></TabsContent>
<TabsContent value='items' forceMount hidden={"items" !== activeTab}></TabsContent>
</Tabs>
<div className='flex items-center justify-center gap-2 md:hidden'> <div className='flex items-center justify-center gap-2 md:hidden'>
<Button variant='outline' size='sm'> <Button variant='outline' size='sm'>
{t("common.discard")} {t("common.discard")}
</Button> </Button>
<Button size='sm'>{t("quotes.edit.buttons.save_quote")}</Button> <Button onClick={handleSubmit((data) => onSubmit(data, false))} size='sm'>
{t("quotes.edit.buttons.save_quote")}
</Button>
</div> </div>
</div> </div>
</form> </form>
</Form> </Form>
<DevTool control={control} />
</> </>
); );
}; };

View File

@ -68,17 +68,19 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
const transform = { const transform = {
input: (value: MoneyValueObject) => { input: (value: MoneyValueObject) => {
if (typeof value !== "object") { if (value === null || value === undefined || typeof value !== "object") {
return value; return value;
} }
const moneyOrError = MoneyValue.create(value); const moneyOrError = MoneyValue.create(value);
if (moneyOrError.isFailure) { if (moneyOrError.isFailure) {
console.error(moneyOrError.error);
throw moneyOrError.error; throw moneyOrError.error;
} }
const result = moneyOrError.object.toString(); const result = moneyOrError.object.toString();
return inputValue.endsWith(",") ? result.replace(/.0$/, ",") : result; return inputValue.endsWith(",") ? result.replace(/.0$/, ",") : result;
}, },
output: ( output: (
@ -90,7 +92,7 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
setInputValue(amount ?? ""); setInputValue(amount ?? "");
const moneyOrError = MoneyValue.createFromFormattedValue(amount, currency.code); const moneyOrError = MoneyValue.createFromFormattedValue(amount, currency.code, scale);
if (moneyOrError.isFailure) { if (moneyOrError.isFailure) {
throw moneyOrError.error; throw moneyOrError.error;
} }
@ -108,20 +110,25 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
rules={rules} rules={rules}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field }) => { render={({ field }) => {
const formattedValue = transform.input(field.value);
//const isEmpty = formattedValue === "" || formattedValue === "0,00";
const isDisabled = disabled || field.disabled;
const isRequired = Boolean(rules?.required ?? false);
return ( return (
<FormItem ref={ref} className={cn(className, "space-y-3")}> <FormItem className={cn(className, "space-y-3")}>
{label && ( {label && <FormLabel label={label} hint={hint} required={isRequired} />}
<FormLabel label={label} hint={hint} required={Boolean(rules?.required ?? false)} />
)}
<FormControl> <FormControl>
<CurrencyInput <CurrencyInput
intlConfig={{ intlConfig={{
locale: language.code, locale: language.code,
useGrouping: true,
}} }}
name={field.name} name={field.name}
//ref={field.ref} <-- no activar que hace cosas raras ref={field.ref} //<-- no activar que hace cosas raras
onBlur={field.onBlur} onBlur={field.onBlur}
disabled={field.disabled} disabled={isDisabled}
readOnly={readOnly} readOnly={readOnly}
className={cn(formCurrencyFieldVariants({ variant, className }))} className={cn(formCurrencyFieldVariants({ variant, className }))}
suffix={` ${currency?.symbol}`} suffix={` ${currency?.symbol}`}
@ -133,9 +140,9 @@ export const FormCurrencyField = React.forwardRef<HTMLInputElement, FormCurrency
decimalScale={scale} decimalScale={scale}
//fixedDecimalLength={scale} <- no activar para que sea más cómodo escribir las cantidades //fixedDecimalLength={scale} <- no activar para que sea más cómodo escribir las cantidades
step={1} step={1}
// { ...field } //{...field}
value={transform.input(field.value)} value={formattedValue}
//onChange={() => {}} //onChange={}
onValueChange={(value, name, values) => onValueChange={(value, name, values) =>
field.onChange(transform.output(value, name, values)) field.onChange(transform.output(value, name, values))
} }

View File

@ -42,7 +42,7 @@ export const FormDatePickerField = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & FormDatePickerFieldProps React.HTMLAttributes<HTMLDivElement> & FormDatePickerFieldProps
>((props: FormDatePickerFieldProps, ref) => { >((props: FormDatePickerFieldProps, ref) => {
const { label, placeholder, hint, description, required, className, name } = props; const { label, placeholder, hint, description, required, disabled, className, name } = props;
const { control } = useFormContext(); const { control } = useFormContext();
//const { locale } = loadDateFnsLocale(); //const { locale } = loadDateFnsLocale();
@ -55,19 +55,22 @@ export const FormDatePickerField = React.forwardRef<
return ( return (
<FormField <FormField
control={control} control={control}
disabled={disabled}
name={name} name={name}
rules={{ required }} rules={{ required }}
render={({ field }) => ( render={({ field }) => (
<FormItem ref={ref} className={cn(className, "flex flex-col")}> <FormItem ref={ref} className={cn(className, "flex flex-col space-y-3")}>
{label && <FormLabel label={label} hint={hint} required={required} />} {label && <FormLabel label={label} hint={hint} required={required} />}
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<FormControl> <FormControl>
<Button <Button
variant={"secondary"} disabled={disabled}
variant={"ghost"}
className={cn( className={cn(
"pl-3 text-left font-normal", "pl-3 text-left font-normal",
"border border-input ",
!field.value && "text-muted-foreground" !field.value && "text-muted-foreground"
)} )}
> >
@ -78,7 +81,12 @@ export const FormDatePickerField = React.forwardRef<
) : ( ) : (
<span>{t("common.pick_date")}</span> <span>{t("common.pick_date")}</span>
)} )}
<CalendarIcon className='w-4 h-4 ml-auto text-' /> <CalendarIcon
className={cn(
"w-4 h-4 ml-auto disabled:opacity-50 ",
disabled ? "text-foreground" : "text-ring"
)}
/>
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>

View File

@ -17,12 +17,14 @@ export const FormLabel = React.forwardRef<
>(({ label, hint, required, ...props }, ref) => { >(({ label, hint, required, ...props }, ref) => {
const { error } = UI.useFormField(); const { error } = UI.useFormField();
const _hint = hint ? hint : required ? t("common.required") : undefined; const _hint = hint ? hint : required ? t("common.required") : "";
const _hintClassName = error ? "text-destructive font-semibold" : ""; const _hintClassName = error ? "text-destructive font-semibold" : "";
return ( return (
<UI.FormLabel ref={ref} className='flex justify-between text-sm' {...props}> <UI.FormLabel ref={ref} className='flex justify-between text-sm' {...props}>
<span className={`block font-semibold ${_hintClassName}`}>{label}</span> <span className={`block font-semibold ${_hintClassName}`}>{label}</span>
{_hint && <span className={`text-sm font-medium ${_hintClassName} `}>{_hint}</span>} {_hint && (
<span className={`text-xs font-medium text-primary ${_hintClassName} `}>{_hint}</span>
)}
</UI.FormLabel> </UI.FormLabel>
); );
}); });

View File

@ -106,17 +106,20 @@ export const FormPercentageField = React.forwardRef<
...rules, ...rules,
}} }}
render={({ field }) => { render={({ field }) => {
const formattedValue = transform.input(field.value);
const isDisabled = disabled || field.disabled;
const isRequired = Boolean(rules?.required ?? false);
return ( return (
<FormItem ref={ref} className={cn(className, "space-y-3")}> <FormItem className={cn(className, "space-y-3")}>
{label && ( {label && <FormLabel label={label} hint={hint} required={isRequired} />}
<FormLabel label={label} hint={hint} required={Boolean(rules?.required ?? false)} />
)}
<FormControl> <FormControl>
<CurrencyInput <CurrencyInput
name={field.name} name={field.name}
//ref={field.ref} <-- no activar que hace cosas raras ref={field.ref} //<-- no activar que hace cosas raras
onBlur={field.onBlur} onBlur={field.onBlur}
disabled={field.disabled} disabled={isDisabled}
readOnly={readOnly} readOnly={readOnly}
className={cn(formPercentageFieldVariants({ variant, className }))} className={cn(formPercentageFieldVariants({ variant, className }))}
groupSeparator='.' groupSeparator='.'
@ -127,7 +130,7 @@ export const FormPercentageField = React.forwardRef<
decimalScale={scale} decimalScale={scale}
step={1} step={1}
//{...field} //{...field}
value={transform.input(field.value)} value={formattedValue}
//onChange={() => {}} //onChange={() => {}}
onValueChange={(value, name, values) => onValueChange={(value, name, values) =>
field.onChange(transform.output(value, name, values)) field.onChange(transform.output(value, name, values))

View File

@ -102,17 +102,20 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
disabled={disabled} disabled={disabled}
rules={rules} rules={rules}
render={({ field }) => { render={({ field }) => {
const formattedValue = transform.input(field.value);
const isDisabled = disabled || field.disabled;
const isRequired = Boolean(rules?.required ?? false);
return ( return (
<FormItem ref={ref} className={cn(className, "space-y-3")}> <FormItem className={cn(className, "space-y-3")}>
{label && ( {label && <FormLabel label={label} hint={hint} required={isRequired} />}
<FormLabel label={label} hint={hint} required={Boolean(rules?.required ?? false)} />
)}
<FormControl> <FormControl>
<CurrencyInput <CurrencyInput
name={field.name} name={field.name}
//ref={field.ref} //<-- no activar que hace cosas raras ref={field.ref} //<-- no activar que hace cosas raras
onBlur={field.onBlur} onBlur={field.onBlur}
disabled={field.disabled} disabled={isDisabled}
readOnly={readOnly} readOnly={readOnly}
className={cn(formQuantityFieldVariants({ variant, className }))} className={cn(formQuantityFieldVariants({ variant, className }))}
groupSeparator='.' groupSeparator='.'
@ -123,7 +126,7 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
decimalScale={scale} decimalScale={scale}
step={1} step={1}
//{...field} //{...field}
value={transform.input(field.value)} value={formattedValue}
//onChange={() => {}} //onChange={() => {}}
onValueChange={(value, name, values) => onValueChange={(value, name, values) =>
field.onChange(transform.output(value, name, values)) field.onChange(transform.output(value, name, values))

View File

@ -76,7 +76,7 @@ export const FormTextAreaField = React.forwardRef<
rules={{ required }} rules={{ required }}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field, fieldState }) => ( render={({ field, fieldState }) => (
<FormItem ref={ref} className={cn(className, "flex flex-col space-y-3")}> <FormItem className={cn(className, "flex flex-col space-y-3")}>
{label && <FormLabel label={label} hint={hint} required={required} />} {label && <FormLabel label={label} hint={hint} required={required} />}
<FormControl className='grow'> <FormControl className='grow'>
{autoSize ? ( {autoSize ? (

View File

@ -3,7 +3,6 @@ import { FormControl, FormDescription, FormField, FormItem, Input, InputProps }
import { cva, type VariantProps } from "class-variance-authority"; import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react"; import * as React from "react";
import { createElement } from "react";
import { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form"; import { FieldPath, FieldValues, UseControllerProps, useFormContext } from "react-hook-form";
import { FormErrorMessage } from "./FormErrorMessage"; import { FormErrorMessage } from "./FormErrorMessage";
import { FormLabel, FormLabelProps } from "./FormLabel"; import { FormLabel, FormLabelProps } from "./FormLabel";
@ -46,20 +45,15 @@ export const FormTextField = React.forwardRef<HTMLInputElement, FormTextFieldPro
disabled, disabled,
defaultValue, defaultValue,
rules, rules,
required,
type, type,
variant, variant,
required,
button,
leadIcon,
trailIcon,
} = props; } = props;
const { control } = useFormContext(); const { control } = useFormContext();
return ( return (
<FormField <FormField
defaultValue={defaultValue}
control={control} control={control}
name={name} name={name}
disabled={disabled} disabled={disabled}
@ -68,63 +62,23 @@ export const FormTextField = React.forwardRef<HTMLInputElement, FormTextFieldPro
...rules, ...rules,
}} }}
render={({ field, fieldState }) => { render={({ field, fieldState }) => {
const isRequired = Boolean(rules?.required ?? required);
return ( return (
<FormItem ref={ref} className={cn(className, "space-y-3")}> <FormItem className={cn(className, "space-y-3")}>
{label && ( {label && <FormLabel label={label} hint={hint} required={isRequired} />}
<FormLabel
label={label} <FormControl className={"block"}>
hint={hint} <Input
required={Boolean(rules?.required ?? required)} type={type}
/> placeholder={placeholder}
)}
<div className={cn(button ? "flex" : null)}>
<div
className={cn( className={cn(
leadIcon ? "relative flex items-stretch flex-grow focus-within:z-10" : "" fieldState.error ? "border-destructive focus-visible:ring-destructive" : "",
formTextFieldVariants({ variant, className })
)} )}
> {...field}
{leadIcon && ( />
<div className='absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none'> </FormControl>
{React.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={type}
placeholder={placeholder}
className={cn(
fieldState.error ? "border-destructive focus-visible:ring-destructive" : "",
formTextFieldVariants({ variant, className })
)}
{...field}
/>
</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>} {description && <FormDescription>{description}</FormDescription>}
<FormErrorMessage /> <FormErrorMessage />

View File

@ -45,6 +45,7 @@ export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
// Redirección si el usuario no está autenticado // Redirección si el usuario no está autenticado
if ((isLoggedInSuccess && !authenticated) || (isProfileSuccess && !profile?.id)) { if ((isLoggedInSuccess && !authenticated) || (isProfileSuccess && !profile?.id)) {
console.debug("Not authenticated, redirecting to:", redirectTo);
return <Navigate to={redirectTo} state={{ from: location }} replace />; return <Navigate to={redirectTo} state={{ from: location }} replace />;
} }

View File

@ -50,18 +50,18 @@ export const calculateQuoteTotals = (quote: any, force: boolean = false) => {
const totalPrice = priceBeforeTaxes.add(taxesPrice).convertScale(2); const totalPrice = priceBeforeTaxes.add(taxesPrice).convertScale(2);
return { return {
subtotalPrice, subtotal_price: subtotalPrice,
discount: quote.discount, discount: quote.discount,
discountPrice, discount_price: discountPrice,
priceBeforeTaxes, before_tax_price: priceBeforeTaxes,
tax, tax,
taxesPrice, tax_price: taxesPrice,
totalPrice, total_price: totalPrice,
}; };
}; };
export const calculateQuoteItemsTotals = (items: any[]) => { export const calculateQuoteItemsTotals = (items: any[]) => {
let totalPrice = MoneyValue.create({ let total_price = MoneyValue.create({
amount: 0, amount: 0,
scale: 2, scale: 2,
}).object; }).object;
@ -69,37 +69,60 @@ export const calculateQuoteItemsTotals = (items: any[]) => {
items && items &&
items.map((item: any) => { items.map((item: any) => {
const quoteItemTotals = calculateQuoteItemTotals(item); const quoteItemTotals = calculateQuoteItemTotals(item);
totalPrice = totalPrice.add(quoteItemTotals.totalPrice); total_price = total_price.add(quoteItemTotals.total_price);
}); });
return totalPrice; return total_price;
}; };
export const calculateQuoteItemTotals = (item: any) => { export const calculateQuoteItemTotals = (item: any) => {
const { quantity: quantity_dto, unit_price: unit_price_dto, discount: discount_dto } = item || {}; const { quantity: quantity_dto, unit_price: unit_price_dto, discount: discount_dto } = item || {};
/*const quantityOrError = Quantity.create(quantity_dto);
if (quantityOrError.isFailure) {
throw quantityOrError.error;
}
const unitPriceOrError = MoneyValue.create(unit_price_dto);
if (unitPriceOrError.isFailure) {
throw unitPriceOrError.error;
}
const discountOrError = Percentage.create(discount_dto);
if (discountOrError.isFailure) {
throw discountOrError.error;
}
return {
quantity: quantityOrError.object,
unitPrice: unitPriceOrError.object,
subtotalPrice: unitPrice.multiply(quantity.toNumber()),
discount: discountOrError.object,
totalPrice: subtotalPrice.subtract(subtotalPrice.percentage(discount.toNumber()))
};
*/
if ( if (
(quantity_dto && quantity_dto.amount === null) || (!quantity_dto || (quantity_dto && quantity_dto.amount === null)) &&
(unit_price_dto && unit_price_dto.amount === null) (!unit_price_dto || (unit_price_dto && unit_price_dto.amount === null)) &&
(!discount_dto || (discount_dto && discount_dto.amount === null))
) { ) {
return { return {
quantity: Quantity.create({ quantity: Quantity.create({
amount: quantity_dto.amount, amount: null,
scale: 0, scale: 0,
}).object, }).object,
unitPrice: MoneyValue.create({ unit_price: MoneyValue.create({
amount: unit_price_dto.amount, amount: null,
scale: 2, scale: 2,
}).object, }).object,
subtotalPrice: MoneyValue.create({ subtotal_price: MoneyValue.create({
amount: null, amount: null,
scale: 2, scale: 2,
}).object, }).object,
discount: Percentage.create({ discount: Percentage.create({
amount: discount_dto.amount, amount: null,
scale: 2, scale: 2,
}).object, }).object,
totalPrice: MoneyValue.create({ total_price: MoneyValue.create({
amount: null, amount: null,
scale: 2, scale: 2,
}).object, }).object,
@ -129,9 +152,9 @@ export const calculateQuoteItemTotals = (item: any) => {
return { return {
quantity, quantity,
unitPrice, unit_price: unitPrice,
subtotalPrice, subtotal_price: subtotalPrice,
discount, discount,
totalPrice, total_price: totalPrice,
}; };
}; };

View File

@ -373,9 +373,9 @@
"desc": "Porcentaje de IVA" "desc": "Porcentaje de IVA"
}, },
"tax_price": { "tax_price": {
"label": "Imp. descuento", "label": "Imp. IVA",
"placeholder": "", "placeholder": "",
"desc": "Importe del descuento" "desc": "Importe del IVA"
}, },
"total_price": { "total_price": {
"label": "Total price", "label": "Total price",

File diff suppressed because it is too large Load Diff

View File

@ -58,7 +58,7 @@ services:
environment: environment:
- NODE_ENV=production - NODE_ENV=production
volumes: volumes:
- backend_logs:/var/log - backend_logs:/logs
- backend_uploads:/api/uploads - backend_uploads:/api/uploads
ports: ports:
- 3001:3001 - 3001:3001

View File

@ -52,7 +52,7 @@
"supertest": "^6.2.2", "supertest": "^6.2.2",
"ts-jest": "^29.2.2", "ts-jest": "^29.2.2",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"typescript": "^5.2.2" "typescript": "^5.5.4"
}, },
"dependencies": { "dependencies": {
"@joi/date": "^2.1.0", "@joi/date": "^2.1.0",
@ -87,6 +87,7 @@
"path": "^0.12.7", "path": "^0.12.7",
"puppeteer": "^22.13.1", "puppeteer": "^22.13.1",
"puppeteer-report": "^3.1.0", "puppeteer-report": "^3.1.0",
"react-hook-form": "7.55.0",
"remove": "^0.1.5", "remove": "^0.1.5",
"response-time": "^2.3.2", "response-time": "^2.3.2",
"sequelize": "^6.37.3", "sequelize": "^6.37.3",

View File

@ -54,8 +54,8 @@ export const initLogger = (rTracer) => {
datePattern: "YYYY-MM-DD", datePattern: "YYYY-MM-DD",
utc: true, utc: true,
level: "error", level: "error",
maxSize: "5m", maxSize: "100m",
maxFiles: "1d", maxFiles: "7d",
}), }),
new DailyRotateFile({ new DailyRotateFile({
dirname: config.isProduction ? "/logs" : ".", dirname: config.isProduction ? "/logs" : ".",
@ -63,8 +63,8 @@ export const initLogger = (rTracer) => {
datePattern: "YYYY-MM-DD", datePattern: "YYYY-MM-DD",
utc: true, utc: true,
level: "debug", level: "debug",
maxSize: "5m", maxSize: "100m",
maxFiles: "1d", maxFiles: "7d",
}), }),
], ],
}); });

View File

@ -5128,6 +5128,11 @@ raw-body@2.5.2:
iconv-lite "0.4.24" iconv-lite "0.4.24"
unpipe "1.0.0" unpipe "1.0.0"
react-hook-form@7.55.0:
version "7.55.0"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.55.0.tgz#df3c80a20a68f6811f49bec3406defaefb6dce80"
integrity sha512-XRnjsH3GVMQz1moZTW53MxfoWN7aDpUg/GpVNc4A3eXRVNdGXfbzJ4vM4aLQ8g6XCUh1nIbx70aaNCl7kxnjog==
react-is@^18.0.0: react-is@^18.0.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
@ -6045,10 +6050,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
typescript@^5.2.2: typescript@^5.5.4:
version "5.5.3" version "5.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
uglify-js@^3.1.4: uglify-js@^3.1.4:
version "3.19.0" version "3.19.0"

View File

@ -139,7 +139,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
} }
const _amount: NullOr<number> = MoneyValue.sanitize(validationResult.object); const _amount: NullOr<number> = MoneyValue.sanitize(validationResult.object);
const _currency = CurrencyData.createFromCode(currencyCode).object.code; const _currency = currencyCode; //CurrencyData.createFromCode(currencyCode).object.code;
const prop = DineroFactory({ const prop = DineroFactory({
amount: Number(_amount), amount: Number(_amount),
@ -153,6 +153,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
public static createFromFormattedValue( public static createFromFormattedValue(
value: NullOr<number | string>, value: NullOr<number | string>,
currencyCode: string, currencyCode: string,
scale: number = MoneyValue.DEFAULT_SCALE,
_options: IMoneyValueOptions = { _options: IMoneyValueOptions = {
locale: defaultMoneyValueOptions.locale, locale: defaultMoneyValueOptions.locale,
} }
@ -160,7 +161,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
if (value === null || value === "") { if (value === null || value === "") {
return MoneyValue.create({ return MoneyValue.create({
amount: null, amount: null,
scale: MoneyValue.DEFAULT_SCALE, scale,
currencyCode, currencyCode,
}); });
} }

View File

@ -15,7 +15,7 @@
"joi-phone-number": "^5.1.1", "joi-phone-number": "^5.1.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"shallow-equal-object": "^1.1.1", "shallow-equal-object": "^1.1.1",
"typescript": "^5.2.2", "typescript": "^5.5.4",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
@ -27,6 +27,6 @@
"eslint-plugin-jest": "^27.4.2", "eslint-plugin-jest": "^27.4.2",
"jest": "^29.7.0", "jest": "^29.7.0",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"typescript": "^5.2.2" "typescript": "^5.5.4"
} }
} }

163
yarn.lock
View File

@ -10,7 +10,7 @@
"@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24" "@jridgewell/trace-mapping" "^0.3.24"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2": "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2":
version "7.24.2" version "7.24.2"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae"
integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==
@ -18,6 +18,15 @@
"@babel/highlight" "^7.24.2" "@babel/highlight" "^7.24.2"
picocolors "^1.0.0" picocolors "^1.0.0"
"@babel/code-frame@^7.12.13":
version "7.26.2"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85"
integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==
dependencies:
"@babel/helper-validator-identifier" "^7.25.9"
js-tokens "^4.0.0"
picocolors "^1.0.0"
"@babel/compat-data@^7.23.5": "@babel/compat-data@^7.23.5":
version "7.24.4" version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a"
@ -127,10 +136,10 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e"
integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==
"@babel/helper-validator-identifier@^7.22.20": "@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.25.9":
version "7.22.20" version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7"
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==
"@babel/helper-validator-option@^7.23.5": "@babel/helper-validator-option@^7.23.5":
version "7.23.5" version "7.23.5"
@ -147,11 +156,11 @@
"@babel/types" "^7.24.0" "@babel/types" "^7.24.0"
"@babel/highlight@^7.24.2": "@babel/highlight@^7.24.2":
version "7.24.2" version "7.25.9"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.25.9.tgz#8141ce68fc73757946f983b343f1231f4691acc6"
integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA== integrity sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==
dependencies: dependencies:
"@babel/helper-validator-identifier" "^7.22.20" "@babel/helper-validator-identifier" "^7.25.9"
chalk "^2.4.2" chalk "^2.4.2"
js-tokens "^4.0.0" js-tokens "^4.0.0"
picocolors "^1.0.0" picocolors "^1.0.0"
@ -679,9 +688,9 @@
"@types/istanbul-lib-report" "*" "@types/istanbul-lib-report" "*"
"@types/jest@^29.5.6": "@types/jest@^29.5.6":
version "29.5.12" version "29.5.14"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5"
integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw== integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==
dependencies: dependencies:
expect "^29.0.0" expect "^29.0.0"
pretty-format "^29.0.0" pretty-format "^29.0.0"
@ -700,16 +709,16 @@
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/lodash@^4.14.200": "@types/lodash@^4.14.200":
version "4.17.6" version "4.17.16"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.6.tgz#193ced6a40c8006cfc1ca3f4553444fb38f0e543" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.16.tgz#94ae78fab4a38d73086e962d0b65c30d816bfb0a"
integrity sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA== integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==
"@types/node@*": "@types/node@*":
version "20.14.10" version "22.14.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.14.0.tgz#d3bfa3936fef0dbacd79ea3eb17d521c628bb47e"
integrity sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ== integrity sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==
dependencies: dependencies:
undici-types "~5.26.4" undici-types "~6.21.0"
"@types/semver@^7.3.12": "@types/semver@^7.3.12":
version "7.5.8" version "7.5.8"
@ -732,9 +741,9 @@
integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==
"@types/yargs@^17.0.8": "@types/yargs@^17.0.8":
version "17.0.32" version "17.0.33"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.32.tgz#030774723a2f7faafebf645f4e5a48371dca6229" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d"
integrity sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog== integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==
dependencies: dependencies:
"@types/yargs-parser" "*" "@types/yargs-parser" "*"
@ -848,9 +857,9 @@ array-union@^2.1.0:
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
async@^3.2.3: async@^3.2.3:
version "3.2.5" version "3.2.6"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
babel-jest@^29.7.0: babel-jest@^29.7.0:
version "29.7.0" version "29.7.0"
@ -932,12 +941,12 @@ brace-expansion@^2.0.1:
dependencies: dependencies:
balanced-match "^1.0.0" balanced-match "^1.0.0"
braces@^3.0.2: braces@^3.0.3:
version "3.0.2" version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies: dependencies:
fill-range "^7.0.1" fill-range "^7.1.1"
browserslist@^4.22.2: browserslist@^4.22.2:
version "4.23.0" version "4.23.0"
@ -949,7 +958,7 @@ browserslist@^4.22.2:
node-releases "^2.0.14" node-releases "^2.0.14"
update-browserslist-db "^1.0.13" update-browserslist-db "^1.0.13"
bs-logger@0.x: bs-logger@^0.2.6:
version "0.2.6" version "0.2.6"
resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==
@ -1184,7 +1193,7 @@ dir-glob@^3.0.1:
dependencies: dependencies:
path-type "^4.0.0" path-type "^4.0.0"
ejs@^3.0.0: ejs@^3.1.10:
version "3.1.10" version "3.1.10"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b"
integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==
@ -1358,10 +1367,10 @@ filelist@^1.0.4:
dependencies: dependencies:
minimatch "^5.0.1" minimatch "^5.0.1"
fill-range@^7.0.1: fill-range@^7.1.1:
version "7.0.1" version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies: dependencies:
to-regex-range "^5.0.1" to-regex-range "^5.0.1"
@ -1666,9 +1675,9 @@ istanbul-reports@^3.1.3:
istanbul-lib-report "^3.0.0" istanbul-lib-report "^3.0.0"
jake@^10.8.5: jake@^10.8.5:
version "10.9.1" version "10.9.2"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.1.tgz#8dc96b7fcc41cb19aa502af506da4e1d56f5e62b" resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f"
integrity sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w== integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==
dependencies: dependencies:
async "^3.2.3" async "^3.2.3"
chalk "^4.0.2" chalk "^4.0.2"
@ -2132,7 +2141,7 @@ locate-path@^5.0.0:
dependencies: dependencies:
p-locate "^4.1.0" p-locate "^4.1.0"
lodash.memoize@4.x: lodash.memoize@^4.1.2:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==
@ -2163,7 +2172,7 @@ make-dir@^4.0.0:
dependencies: dependencies:
semver "^7.5.3" semver "^7.5.3"
make-error@1.x: make-error@^1.3.6:
version "1.3.6" version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
@ -2202,11 +2211,11 @@ merge2@^1.3.0, merge2@^1.4.1:
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4: micromatch@^4.0.4:
version "4.0.5" version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies: dependencies:
braces "^3.0.2" braces "^3.0.3"
picomatch "^2.3.1" picomatch "^2.3.1"
mimic-fn@^2.0.0, mimic-fn@^2.1.0: mimic-fn@^2.0.0, mimic-fn@^2.1.0:
@ -2412,9 +2421,9 @@ path-type@^4.0.0:
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
picocolors@^1.0.0: picocolors@^1.0.0:
version "1.0.0" version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1:
version "2.3.1" version "2.3.1"
@ -2474,9 +2483,9 @@ queue-microtask@^1.2.2:
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
react-is@^18.0.0: react-is@^18.0.0:
version "18.2.0" version "18.3.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
read-pkg@^4.0.1: read-pkg@^4.0.1:
version "4.0.1" version "4.0.1"
@ -2559,10 +2568,10 @@ semver@^7.3.7, semver@^7.5.4:
dependencies: dependencies:
lru-cache "^6.0.0" lru-cache "^6.0.0"
semver@^7.5.3: semver@^7.5.3, semver@^7.7.1:
version "7.6.2" version "7.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
set-blocking@^2.0.0: set-blocking@^2.0.0:
version "2.0.0" version "2.0.0"
@ -2809,19 +2818,20 @@ tree-kill@^1.1.0:
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
ts-jest@^29.1.1: ts-jest@^29.1.1:
version "29.2.1" version "29.3.1"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.1.tgz#9a460bb27446d141c48a17cf24f060dbe9b58254" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.3.1.tgz#2e459e1f94a833bd8216ba4b045fac948e265937"
integrity sha512-7obwtH5gw0b0XZi0wmprCSvGSvHliMBI47lPnU47vmbxWS6B+v1X94yWFo1f1vt9k/he+gttsrXjkxmgY41XNQ== integrity sha512-FT2PIRtZABwl6+ZCry8IY7JZ3xMuppsEV9qFVHOVe8jDzggwUZ9TsM4chyJxL9yi6LvkqcZYU3LmapEE454zBQ==
dependencies: dependencies:
bs-logger "0.x" bs-logger "^0.2.6"
ejs "^3.0.0" ejs "^3.1.10"
fast-json-stable-stringify "2.x" fast-json-stable-stringify "^2.1.0"
jest-util "^29.0.0" jest-util "^29.0.0"
json5 "^2.2.3" json5 "^2.2.3"
lodash.memoize "4.x" lodash.memoize "^4.1.2"
make-error "1.x" make-error "^1.3.6"
semver "^7.5.3" semver "^7.7.1"
yargs-parser "^21.0.1" type-fest "^4.38.0"
yargs-parser "^21.1.1"
tslib@^1.8.1, tslib@^1.9.0: tslib@^1.8.1, tslib@^1.9.0:
version "1.14.1" version "1.14.1"
@ -2845,15 +2855,20 @@ type-fest@^0.21.3:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
typescript@^5.2.2: type-fest@^4.38.0:
version "5.5.3" version "4.39.1"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.39.1.tgz#7521f6944e279abaf79cf60cfbc4823f4858083e"
integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== integrity sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==
undici-types@~5.26.4: typescript@^5.2.2, typescript@^5.5.4:
version "5.26.5" version "5.8.3"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
undici-types@~6.21.0:
version "6.21.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
update-browserslist-db@^1.0.13: update-browserslist-db@^1.0.13:
version "1.0.13" version "1.0.13"
@ -2969,7 +2984,7 @@ yargs-parser@^11.1.1:
camelcase "^5.0.0" camelcase "^5.0.0"
decamelize "^1.2.0" decamelize "^1.2.0"
yargs-parser@^21.0.1, yargs-parser@^21.1.1: yargs-parser@^21.1.1:
version "21.1.1" version "21.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==