diff --git a/client/src/app/quotes/components/QuotePricesResume.tsx b/client/src/app/quotes/components/QuotePricesResume.tsx index 10f3942..b8d00cc 100644 --- a/client/src/app/quotes/components/QuotePricesResume.tsx +++ b/client/src/app/quotes/components/QuotePricesResume.tsx @@ -1,14 +1,18 @@ import { DollarSign } from "lucide-react"; +import { useLocalization } from "@/lib/hooks"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, Separator } from "@/ui"; import { useFormContext } from "react-hook-form"; export const QuotePricesResume = () => { const { getValues } = useFormContext(); - //const { formatNumber } = useLocalization({ locale: "es-ES" }); + const { formatNumber, formatCurrency, formatPercentage } = useLocalization(); - const subtotal_price = getValues("subtotal_price"); - const discount = getValues("discount"); + console.log(getValues()); + + const subtotal_price = formatNumber(getValues("subtotal_price")); + const discount = formatPercentage(getValues("discount")); + const total_price = formatCurrency(getValues("total_price")); return ( @@ -16,9 +20,9 @@ export const QuotePricesResume = () => {
Importe neto - - {subtotal_price.amount / 100} - + + {subtotal_price} +
@@ -26,16 +30,16 @@ export const QuotePricesResume = () => {
Descuento - - {discount.amount / 100} + + {discount} %
Imp. descuento - - {subtotal_price.amount / 100} - + + {subtotal_price} +
@@ -43,15 +47,15 @@ export const QuotePricesResume = () => {
IVA - - {discount.amount / 100} + + {discount} %
Importe IVA - - {subtotal_price.amount / 100} + + {subtotal_price}
@@ -60,9 +64,9 @@ export const QuotePricesResume = () => {
Importe total - - {subtotal_price.amount / 100} - + + {total_price} +
diff --git a/client/src/app/quotes/edit.tsx b/client/src/app/quotes/edit.tsx index 1cbba9c..cdaf214 100644 --- a/client/src/app/quotes/edit.tsx +++ b/client/src/app/quotes/edit.tsx @@ -24,6 +24,34 @@ import { useQuotes } from "./hooks"; type QuoteDataForm = IGetQuote_Response_DTO; +const recalculateItemTotals = (items, setValue) => { + let quoteSubtotal = MoneyValue.create({ + amount: 0, + scale: 4, + }).object; + + items.forEach((item, index) => { + const itemTotals = calculateItemTotals(item); + quoteSubtotal = quoteSubtotal.add(itemTotals.totalPrice); + setValue(`items.${index}.subtotal_price`, itemTotals.subtotalPrice.toObject()); + setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject()); + }); + + setValue("subtotal_price", quoteSubtotal.convertScale(2).toObject()); +}; + +const updateCurrency = (value, setQuoteCurrency) => { + setQuoteCurrency( + CurrencyData.createFromCode(value.currency_code ?? CurrencyData.DEFAULT_CURRENCY_CODE).object + ); +}; + +const updateLanguage = (value, setQuoteLanguage) => { + setQuoteLanguage( + Language.createFromCode(value.lang_code ?? Language.DEFAULT_LANGUAGE_CODE).object + ); +}; + // eslint-disable-next-line @typescript-eslint/no-unused-vars export const QuoteEdit = () => { const navigate = useNavigate(); @@ -111,6 +139,7 @@ export const QuoteEdit = () => { mode: "onBlur", values: data, defaultValues, + //shouldUnregister: true, }); const { watch, getValues, setValue, formState } = form; @@ -146,7 +175,7 @@ export const QuoteEdit = () => { }); }; - useEffect(() => { + /*useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { unsubscribe } = watch((_, { name, type }) => { const value = getValues(); @@ -183,7 +212,7 @@ export const QuoteEdit = () => { }); // Recálculo completo - setValue("subtotal_price", quoteSubtotal.toObject()); + setValue("subtotal_price", quoteSubtotal.convertScale(2).toObject()); } if (name.endsWith("quantity") || name.endsWith("unit_price") || name.endsWith("discount")) { @@ -197,11 +226,66 @@ export const QuoteEdit = () => { setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject()); // Recálculo completo + let quoteSubtotal = MoneyValue.create({ + amount: 0, + scale: 4, + }).object; + + items && + items.map((item) => { + const itemTotals = calculateItemTotals(item); + quoteSubtotal = quoteSubtotal.add(itemTotals.totalPrice); + setValue(`items.${index}.subtotal_price`, itemTotals.subtotalPrice.toObject()); + setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject()); + }); + + // Recálculo completo + setValue("subtotal_price", quoteSubtotal.convertScale(2).toObject()); } } }); return () => unsubscribe(); - }, [watch, getValues, setValue]); + }, [watch, getValues, setValue]);*/ + + useEffect(() => { + const { unsubscribe } = watch((_, { name }) => { + const value = getValues(); + + if (name) { + switch (true) { + case name === "currency_code": + updateCurrency(value, setQuoteCurrency); + break; + + case name === "lang_code": + updateLanguage(value, setQuoteLanguage); + break; + + case name === "items": + recalculateItemTotals(value.items, setValue); + break; + + case name.endsWith("quantity") || + name.endsWith("unit_price") || + name.endsWith("discount"): { + const [, indexString] = String(name).split("."); + const index = parseInt(indexString); + + const itemTotals = calculateItemTotals(value.items[index]); + setValue(`items.${index}.subtotal_price`, itemTotals.subtotalPrice.toObject()); + setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject()); + + recalculateItemTotals(value.items, setValue); + break; + } + + default: + break; + } + } + }); + return () => unsubscribe(); + }, [watch, getValues, setValue, setQuoteCurrency, setQuoteLanguage]); if (isSubmitting) { return ; diff --git a/client/src/lib/hooks/useLocalization/useLocatlization.tsx b/client/src/lib/hooks/useLocalization/useLocatlization.tsx index db6b4e2..0f135b2 100644 --- a/client/src/lib/hooks/useLocalization/useLocatlization.tsx +++ b/client/src/lib/hooks/useLocalization/useLocatlization.tsx @@ -1,52 +1,88 @@ /* https://github.com/mayank8aug/use-localization/blob/main/src/index.ts */ -import { useCallback, useMemo } from "react"; -import { LocaleToCurrencyTable, rtlLangsList } from "./utils"; +import { IMoney, IPercentage, IQuantity } from "@/lib/types"; +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; type UseLocalizationProps = { locale: string; }; -export const useLocalization = (props: UseLocalizationProps) => { +const adjustPrecision = ({ amount, scale }: { amount: number; scale: number }) => { + const factor = 10 ** scale; + return Number(amount) / factor; +}; + +export const useLocalization = () => { + const { i18n } = useTranslation(); + + return useCustomLocalization({ + locale: i18n.language, + }); +}; + +export const useCustomLocalization = (props: UseLocalizationProps) => { const { locale } = props; - const [lang, loc] = locale.split("-"); - - //const { i18n } = useTranslation(); - - // Obtener el idioma actual - // const currentLanguage = i18n.language; const formatCurrency = useCallback( - (value: number) => { + (value: IMoney | null) => { + if (value === null || value === undefined) { + return ""; + } + + const { amount, scale, currency_code } = value; + return new Intl.NumberFormat(locale, { style: "currency", - currency: LocaleToCurrencyTable[locale], - }).format(value); + currency: currency_code, + currencyDisplay: "symbol", + maximumFractionDigits: scale, + }).format(amount === null ? 0 : amount); }, [locale] ); const formatNumber = useCallback( - (value: number) => { - return new Intl.NumberFormat(locale).format(value); + (value: IMoney | IPercentage | IQuantity | null) => { + if (value === null || value === undefined) { + return ""; + } + + const { amount, scale } = value; + + const result = new Intl.NumberFormat(locale, { + minimumSignificantDigits: scale, + maximumSignificantDigits: scale, + minimumFractionDigits: scale, + useGrouping: true, + }).format(amount === null ? 0 : adjustPrecision({ amount, scale })); + + console.log(value, result); + + return result; }, [locale] ); - const flag = useMemo( - () => - typeof String.fromCodePoint !== "undefined" - ? loc - .toUpperCase() - .replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397)) - : "", - [loc] + const formatPercentage = useCallback( + (value: IPercentage | null) => { + if (value === null || value === undefined) { + return ""; + } + + const { amount, scale } = value; + + return new Intl.NumberFormat(locale, { + style: "decimal", + minimumFractionDigits: scale, + }).format(amount === null ? 0 : amount); + }, + [locale] ); return { formatCurrency, formatNumber, - flag, - isRTL: rtlLangsList.includes(lang), + formatPercentage, }; }; diff --git a/client/src/lib/types.ts b/client/src/lib/types.ts index e7e941b..263b2ca 100644 --- a/client/src/lib/types.ts +++ b/client/src/lib/types.ts @@ -1,9 +1,5 @@ -import { - IMoney_Response_DTO, - IPercentage_Response_DTO, - IQuantity_Response_DTO, -} from "@shared/contexts"; +import { IMoney_DTO, IPercentage_DTO, IQuantity_DTO } from "@shared/contexts"; -export interface IMoney extends IMoney_Response_DTO {} -export interface IQuantity extends IQuantity_Response_DTO {} -export interface IPercentage extends IPercentage_Response_DTO {} +export interface IMoney extends IMoney_DTO {} +export interface IQuantity extends IQuantity_DTO {} +export interface IPercentage extends IPercentage_DTO {} diff --git a/server/src/contexts/sales/infrastructure/express/controllers/quotes/getQuote/presenter/GetQuote.presenter.ts b/server/src/contexts/sales/infrastructure/express/controllers/quotes/getQuote/presenter/GetQuote.presenter.ts index 5afe120..ed2a944 100644 --- a/server/src/contexts/sales/infrastructure/express/controllers/quotes/getQuote/presenter/GetQuote.presenter.ts +++ b/server/src/contexts/sales/infrastructure/express/controllers/quotes/getQuote/presenter/GetQuote.presenter.ts @@ -26,9 +26,9 @@ export const GetQuotePresenter: IGetQuotePresenter = { validity: quote.validity.toString(), notes: quote.notes.toString(), - subtotal_price: quote.subtotalPrice.toObject(), - discount: quote.discount.toObject(), - total_price: quote.totalPrice.toObject(), + subtotal_price: quote.subtotalPrice.convertScale(2).toObject(), + discount: quote.discount.convertScale(2).toObject(), + total_price: quote.totalPrice.convertScale(2).toObject(), items: quoteItemPresenter(quote.items, context), dealer_id: quote.dealerId.toString(), @@ -45,10 +45,10 @@ const quoteItemPresenter = ( ? items.items.map((item: QuoteItem) => ({ article_id: item.articleId.toString(), description: item.description.toString(), - quantity: item.quantity.toObject(), - unit_price: item.unitPrice.toObject(), - subtotal_price: item.subtotalPrice.toObject(), - discount: item.discount.toObject(), - total_price: item.totalPrice.toObject(), + quantity: item.quantity.convertScale(2).toObject(), + unit_price: item.unitPrice.convertScale(4).toObject(), + subtotal_price: item.subtotalPrice.convertScale(4).toObject(), + discount: item.discount.convertScale(2).toObject(), + total_price: item.totalPrice.convertScale(4).toObject(), })) : []; diff --git a/server/src/contexts/sales/infrastructure/sequelize/quote.model.ts b/server/src/contexts/sales/infrastructure/sequelize/quote.model.ts index 6bcae77..7c1a16e 100644 --- a/server/src/contexts/sales/infrastructure/sequelize/quote.model.ts +++ b/server/src/contexts/sales/infrastructure/sequelize/quote.model.ts @@ -50,9 +50,9 @@ export class Quote_Model extends Model< declare notes: CreationOptional; declare validity: CreationOptional; - declare subtotal_price: CreationOptional; + declare subtotal_price: CreationOptional; declare discount: CreationOptional; - declare total_price: CreationOptional; + declare total_price: CreationOptional; declare items: NonAttribute; declare dealer: NonAttribute;