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

View File

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

View File

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

View File

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

View File

@ -1,25 +1,84 @@
import { FormPercentageField } from "@/components";
import { useLocalization } from "@/lib/hooks";
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 { useMemo } from "react";
import { useFormContext } from "react-hook-form";
import { useFormContext, useWatch } from "react-hook-form";
export const QuotePricesResume = () => {
const { watch, register, formState } = useFormContext();
const calculateQuoteTotals = (prices: any) => {
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 currency_code = watch("currency_code");
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 pricesWatch = useWatch({ control, name: ["subtotal_price", "discount", "tax"] });
const currency_symbol = useMemo(() => {
const currencyOrError = CurrencyData.createFromCode(currency_code);
return currencyOrError.isSuccess ? currencyOrError.object.symbol : "";
}, [currency_code]);
const totals = calculateQuoteTotals(pricesWatch);
const subtotal_price = formatNumber(totals.subtotalPrice);
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 (
<Card className='w-full bg-muted'>
@ -31,6 +90,7 @@ export const QuotePricesResume = () => {
</CardDescription>
<CardTitle className='flex items-baseline justify-end text-2xl tabular-nums'>
{subtotal_price}
<span className='ml-1 text-lg tracking-normal'>{currency_symbol}</span>
</CardTitle>
</div>
@ -48,6 +108,9 @@ export const QuotePricesResume = () => {
{...register("discount", {
required: false,
})}
onChange={(value) => {
console.log("discount", value);
}}
/>
</div>
<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;
}) => {
const { toast } = useToast();
const { control, register } = useFormContext();
const {
control,
register,
watch,
setValue,
formState: { isDirty },
} = useFormContext();
const [pickerMode] = useState<"dialog" | "panel">("dialog");
@ -43,6 +49,75 @@ export const QuoteDetailsCardEditor = ({
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(
[
/*{
@ -84,7 +159,8 @@ export const QuoteDetailsCardEditor = ({
/>
);
},
size: 500,
minSize: 200,
size: 400,
},
{
id: "quantity" as const,
@ -101,6 +177,7 @@ export const QuoteDetailsCardEditor = ({
/>
);
},
size: 75,
},
{
id: "unit_price" as const,
@ -119,6 +196,7 @@ export const QuoteDetailsCardEditor = ({
/>
);
},
size: 125,
},
{
id: "subtotal_price" as const,
@ -129,6 +207,7 @@ export const QuoteDetailsCardEditor = ({
cell: ({ row: { index } }) => {
return (
<FormCurrencyField
variant='ghost'
currency={currency}
language={language}
scale={2}
@ -138,6 +217,7 @@ export const QuoteDetailsCardEditor = ({
/>
);
},
size: 150,
},
{
id: "discount" as const,
@ -154,6 +234,7 @@ export const QuoteDetailsCardEditor = ({
/>
);
},
size: 100,
},
{
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")}
footerActions={
<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")}
</Button>
<SubmitButton size='sm' label={t("common.continue")}></SubmitButton>

View File

@ -9,8 +9,9 @@ import {
import { calculateQuoteItemTotals, calculateQuoteTotals } from "@/lib/calc";
import { useUnsavedChangesNotifier } from "@/lib/hooks";
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 { DevTool } from "@hookform/devtools";
import {
CurrencyData,
IGetQuote_QuoteItem_Response_DTO,
@ -18,7 +19,8 @@ import {
Language,
} from "@shared/contexts";
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 { useNavigate } from "react-router-dom";
import { QuotePricesResume } from "./components";
@ -34,17 +36,21 @@ export const QuoteEdit = () => {
const quoteId = useUrlId();
const { toast } = useToast();
const [activeTab, setActiveTab] = useState("general");
const [quoteCurrency, setQuoteCurrency] = useState<CurrencyData>(
CurrencyData.createDefaultCode().object
);
const [quoteLanguage, setQuoteLanguage] = useState<Language>(Language.createDefaultCode().object);
const { useOne, useUpdate } = useQuotes();
const { data, status, error: queryError } = useOne(quoteId);
// 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.
// 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(
() => ({
date: "",
@ -59,21 +65,21 @@ export const QuoteEdit = () => {
subtotal_price: {
amount: undefined,
scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code,
currency_code: "",
},
discount: {
amount: undefined,
amount: 0,
scale: 0,
},
discount_price: {
amount: undefined,
scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code,
amount: 0,
scale: 0,
currency_code: "",
},
before_tax_price: {
amount: undefined,
scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code,
currency_code: "",
},
tax: {
amount: undefined,
@ -82,62 +88,232 @@ export const QuoteEdit = () => {
tax_price: {
amount: undefined,
scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code,
currency_code: "",
},
total_price: {
amount: undefined,
scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code,
currency_code: "",
},
items: [
{
id_article: "",
description: "",
quantity: {
amount: null,
amount: undefined,
scale: 2,
},
unit_price: {
amount: null,
amount: undefined,
scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code,
currency_code: "",
},
subtotal_price: {
amount: null,
amount: undefined,
scale: 2,
currency_code: data?.currency_code ?? quoteCurrency.code,
currency_code: "",
},
discount: {
amount: null,
amount: undefined,
scale: 2,
},
total_price: {
amount: null,
amount: undefined,
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 form = useForm<QuoteDataForm>({
mode: "onBlur",
values: data,
defaultValues,
//shouldUnregister: true,
defaultValues, // lo ideal es usar solo defaultValues y usar reset(data) cuando llegue la data del useOne.
//values: data,
//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;
useUnsavedChangesNotifier({
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) => {
// Transformación del form -> typo de request
@ -159,94 +335,20 @@ export const QuoteEdit = () => {
});
};
useEffect(() => {
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 = () => {
const handleClose = useCallback(() => {
navigate("/quotes", {
state: {
id: quoteId,
},
});
};
}, [navigate, quoteId]);
if (isSubmitting || isPending) {
//return <LoadingOverlay title='Guardando cotización' />;
}
// Reset form when data is fetched
useEffect(() => {
if (status === "success" && data) {
reset(data);
}
}, [status, data, reset]);
if (status === "error") {
return <ErrorOverlay errorMessage={queryError.message} />;
@ -294,42 +396,25 @@ export const QuoteEdit = () => {
</div>
<QuoteGeneralCardEditor />
<QuotePricesResume />
<QuotePricesResume currency={quoteCurrency} />
<QuoteDetailsCardEditor
currency={quoteCurrency}
language={quoteLanguage}
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'>
<Button variant='outline' size='sm'>
{t("common.discard")}
</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>
</form>
</Form>
<DevTool control={control} />
</>
);
};

View File

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

View File

@ -42,7 +42,7 @@ export const FormDatePickerField = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & FormDatePickerFieldProps
>((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 { locale } = loadDateFnsLocale();
@ -55,19 +55,22 @@ export const FormDatePickerField = React.forwardRef<
return (
<FormField
control={control}
disabled={disabled}
name={name}
rules={{ required }}
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} />}
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={"secondary"}
disabled={disabled}
variant={"ghost"}
className={cn(
"pl-3 text-left font-normal",
"border border-input ",
!field.value && "text-muted-foreground"
)}
>
@ -78,7 +81,12 @@ export const FormDatePickerField = React.forwardRef<
) : (
<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>
</FormControl>
</PopoverTrigger>

View File

@ -17,12 +17,14 @@ export const FormLabel = React.forwardRef<
>(({ label, hint, required, ...props }, ref) => {
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" : "";
return (
<UI.FormLabel ref={ref} className='flex justify-between text-sm' {...props}>
<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>
);
});

View File

@ -106,17 +106,20 @@ export const FormPercentageField = React.forwardRef<
...rules,
}}
render={({ field }) => {
const formattedValue = transform.input(field.value);
const isDisabled = disabled || field.disabled;
const isRequired = Boolean(rules?.required ?? false);
return (
<FormItem ref={ref} className={cn(className, "space-y-3")}>
{label && (
<FormLabel label={label} hint={hint} required={Boolean(rules?.required ?? false)} />
)}
<FormItem className={cn(className, "space-y-3")}>
{label && <FormLabel label={label} hint={hint} required={isRequired} />}
<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}
disabled={isDisabled}
readOnly={readOnly}
className={cn(formPercentageFieldVariants({ variant, className }))}
groupSeparator='.'
@ -127,7 +130,7 @@ export const FormPercentageField = React.forwardRef<
decimalScale={scale}
step={1}
//{...field}
value={transform.input(field.value)}
value={formattedValue}
//onChange={() => {}}
onValueChange={(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}
rules={rules}
render={({ field }) => {
const formattedValue = transform.input(field.value);
const isDisabled = disabled || field.disabled;
const isRequired = Boolean(rules?.required ?? false);
return (
<FormItem ref={ref} className={cn(className, "space-y-3")}>
{label && (
<FormLabel label={label} hint={hint} required={Boolean(rules?.required ?? false)} />
)}
<FormItem className={cn(className, "space-y-3")}>
{label && <FormLabel label={label} hint={hint} required={isRequired} />}
<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}
disabled={isDisabled}
readOnly={readOnly}
className={cn(formQuantityFieldVariants({ variant, className }))}
groupSeparator='.'
@ -123,7 +126,7 @@ export const FormQuantityField = React.forwardRef<HTMLInputElement, FormQuantity
decimalScale={scale}
step={1}
//{...field}
value={transform.input(field.value)}
value={formattedValue}
//onChange={() => {}}
onValueChange={(value, name, values) =>
field.onChange(transform.output(value, name, values))

View File

@ -76,7 +76,7 @@ export const FormTextAreaField = React.forwardRef<
rules={{ required }}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
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} />}
<FormControl className='grow'>
{autoSize ? (

View File

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

View File

@ -45,6 +45,7 @@ export const ProtectedRoute = ({ children }: ProctectRouteProps) => {
// Redirección si el usuario no está autenticado
if ((isLoggedInSuccess && !authenticated) || (isProfileSuccess && !profile?.id)) {
console.debug("Not authenticated, redirecting to:", redirectTo);
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);
return {
subtotalPrice,
subtotal_price: subtotalPrice,
discount: quote.discount,
discountPrice,
priceBeforeTaxes,
discount_price: discountPrice,
before_tax_price: priceBeforeTaxes,
tax,
taxesPrice,
totalPrice,
tax_price: taxesPrice,
total_price: totalPrice,
};
};
export const calculateQuoteItemsTotals = (items: any[]) => {
let totalPrice = MoneyValue.create({
let total_price = MoneyValue.create({
amount: 0,
scale: 2,
}).object;
@ -69,37 +69,60 @@ export const calculateQuoteItemsTotals = (items: any[]) => {
items &&
items.map((item: any) => {
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) => {
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 (
(quantity_dto && quantity_dto.amount === null) ||
(unit_price_dto && unit_price_dto.amount === null)
(!quantity_dto || (quantity_dto && quantity_dto.amount === null)) &&
(!unit_price_dto || (unit_price_dto && unit_price_dto.amount === null)) &&
(!discount_dto || (discount_dto && discount_dto.amount === null))
) {
return {
quantity: Quantity.create({
amount: quantity_dto.amount,
amount: null,
scale: 0,
}).object,
unitPrice: MoneyValue.create({
amount: unit_price_dto.amount,
unit_price: MoneyValue.create({
amount: null,
scale: 2,
}).object,
subtotalPrice: MoneyValue.create({
subtotal_price: MoneyValue.create({
amount: null,
scale: 2,
}).object,
discount: Percentage.create({
amount: discount_dto.amount,
amount: null,
scale: 2,
}).object,
totalPrice: MoneyValue.create({
total_price: MoneyValue.create({
amount: null,
scale: 2,
}).object,
@ -129,9 +152,9 @@ export const calculateQuoteItemTotals = (item: any) => {
return {
quantity,
unitPrice,
subtotalPrice,
unit_price: unitPrice,
subtotal_price: subtotalPrice,
discount,
totalPrice,
total_price: totalPrice,
};
};

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -5128,6 +5128,11 @@ raw-body@2.5.2:
iconv-lite "0.4.24"
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:
version "18.2.0"
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"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
typescript@^5.2.2:
version "5.5.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa"
integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==
typescript@^5.5.4:
version "5.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.2.tgz#8170b3702f74b79db2e5a96207c15e65807999e4"
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
uglify-js@^3.1.4:
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 _currency = CurrencyData.createFromCode(currencyCode).object.code;
const _currency = currencyCode; //CurrencyData.createFromCode(currencyCode).object.code;
const prop = DineroFactory({
amount: Number(_amount),
@ -153,6 +153,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
public static createFromFormattedValue(
value: NullOr<number | string>,
currencyCode: string,
scale: number = MoneyValue.DEFAULT_SCALE,
_options: IMoneyValueOptions = {
locale: defaultMoneyValueOptions.locale,
}
@ -160,7 +161,7 @@ export class MoneyValue extends ValueObject<Dinero> implements IMoneyValue {
if (value === null || value === "") {
return MoneyValue.create({
amount: null,
scale: MoneyValue.DEFAULT_SCALE,
scale,
currencyCode,
});
}

View File

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