This commit is contained in:
David Arranz 2024-07-18 20:26:44 +02:00
parent aabe3579ab
commit 5e4d58c112
6 changed files with 183 additions and 63 deletions

View File

@ -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 (
<Card className='w-full'>
@ -16,9 +20,9 @@ export const QuotePricesResume = () => {
<div className='grid flex-1 h-16 grid-cols-1 auto-rows-max'>
<div className='grid gap-1 font-semibold text-muted-foreground'>
<CardDescription className='text-sm'>Importe neto</CardDescription>
<CardTitle className='items-baseline gap-1 text-3xl tabular-nums'>
{subtotal_price.amount / 100}
<span className='text-base tracking-normal'></span>
<CardTitle className='flex items-baseline text-2xl tabular-nums'>
{subtotal_price}
<span className='ml-1 text-lg tracking-normal'></span>
</CardTitle>
</div>
</div>
@ -26,16 +30,16 @@ export const QuotePricesResume = () => {
<div className='grid flex-1 h-16 grid-cols-2 gap-2 auto-rows-max'>
<div className='grid gap-1 font-medium text-muted-foreground'>
<CardDescription className='text-sm'>Descuento</CardDescription>
<CardTitle className='flex items-baseline gap-1 text-2xl tabular-nums'>
{discount.amount / 100}
<CardTitle className='flex items-baseline gap-1 text-xl tabular-nums'>
{discount}
<span className='text-base tracking-normal'>%</span>
</CardTitle>
</div>
<div className='grid gap-1 font-semibold text-muted-foreground'>
<CardDescription className='text-sm'>Imp. descuento</CardDescription>
<CardTitle className='flex items-baseline gap-1 text-3xl tabular-nums'>
{subtotal_price.amount / 100}
<span className='text-base font-medium tracking-normal'></span>
<CardTitle className='flex items-baseline text-2xl tabular-nums'>
{subtotal_price}
<span className='ml-1 text-lg tracking-normal'></span>
</CardTitle>
</div>
</div>
@ -43,15 +47,15 @@ export const QuotePricesResume = () => {
<div className='grid flex-1 h-16 grid-cols-2 gap-2 auto-rows-max'>
<div className='grid gap-1 font-medium text-muted-foreground'>
<CardDescription className='text-sm'>IVA</CardDescription>
<CardTitle className='flex items-baseline gap-1 text-2xl tabular-nums'>
{discount.amount / 100}
<CardTitle className='flex items-baseline gap-1 text-xl tabular-nums'>
{discount}
<span className='text-base tracking-normal'>%</span>
</CardTitle>
</div>
<div className='grid gap-1 font-semibold text-muted-foreground'>
<CardDescription className='text-sm'>Importe IVA</CardDescription>
<CardTitle className='flex items-baseline gap-1 text-3xl tabular-nums'>
{subtotal_price.amount / 100}
<CardTitle className='flex items-baseline gap-1 text-2xl tabular-nums'>
{subtotal_price}
<span className='text-base font-medium tracking-normal'></span>
</CardTitle>
</div>
@ -60,9 +64,9 @@ export const QuotePricesResume = () => {
<div className='grid flex-1 h-16 grid-cols-1 auto-rows-max'>
<div className='grid gap-0'>
<CardDescription className='text-sm font-semibold'>Importe total</CardDescription>
<CardTitle className='flex items-baseline gap-1 text-4xl tabular-nums'>
{subtotal_price.amount / 100}
<span className='text-base font-medium tracking-normal'></span>
<CardTitle className='flex items-baseline gap-1 text-3xl tabular-nums'>
{total_price}
<span className='ml-1 text-lg tracking-normal'></span>
</CardTitle>
</div>
</div>

View File

@ -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 <LoadingOverlay title='Guardando cotización' />;

View File

@ -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,
};
};

View File

@ -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 {}

View File

@ -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(),
}))
: [];

View File

@ -50,9 +50,9 @@ export class Quote_Model extends Model<
declare notes: CreationOptional<string>;
declare validity: CreationOptional<string>;
declare subtotal_price: CreationOptional<number>;
declare subtotal_price: CreationOptional<number | null>;
declare discount: CreationOptional<number | null>;
declare total_price: CreationOptional<number>;
declare total_price: CreationOptional<number | null>;
declare items: NonAttribute<QuoteItem_Model[]>;
declare dealer: NonAttribute<Dealer_Model>;