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 { DollarSign } from "lucide-react";
import { useLocalization } from "@/lib/hooks";
import { Card, CardContent, CardDescription, CardHeader, CardTitle, Separator } from "@/ui"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, Separator } from "@/ui";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
export const QuotePricesResume = () => { export const QuotePricesResume = () => {
const { getValues } = useFormContext(); const { getValues } = useFormContext();
//const { formatNumber } = useLocalization({ locale: "es-ES" }); const { formatNumber, formatCurrency, formatPercentage } = useLocalization();
const subtotal_price = getValues("subtotal_price"); console.log(getValues());
const discount = getValues("discount");
const subtotal_price = formatNumber(getValues("subtotal_price"));
const discount = formatPercentage(getValues("discount"));
const total_price = formatCurrency(getValues("total_price"));
return ( return (
<Card className='w-full'> <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 flex-1 h-16 grid-cols-1 auto-rows-max'>
<div className='grid gap-1 font-semibold text-muted-foreground'> <div className='grid gap-1 font-semibold text-muted-foreground'>
<CardDescription className='text-sm'>Importe neto</CardDescription> <CardDescription className='text-sm'>Importe neto</CardDescription>
<CardTitle className='items-baseline gap-1 text-3xl tabular-nums'> <CardTitle className='flex items-baseline text-2xl tabular-nums'>
{subtotal_price.amount / 100} {subtotal_price}
<span className='text-base tracking-normal'></span> <span className='ml-1 text-lg tracking-normal'></span>
</CardTitle> </CardTitle>
</div> </div>
</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 flex-1 h-16 grid-cols-2 gap-2 auto-rows-max'>
<div className='grid gap-1 font-medium text-muted-foreground'> <div className='grid gap-1 font-medium text-muted-foreground'>
<CardDescription className='text-sm'>Descuento</CardDescription> <CardDescription className='text-sm'>Descuento</CardDescription>
<CardTitle className='flex items-baseline gap-1 text-2xl tabular-nums'> <CardTitle className='flex items-baseline gap-1 text-xl tabular-nums'>
{discount.amount / 100} {discount}
<span className='text-base tracking-normal'>%</span> <span className='text-base tracking-normal'>%</span>
</CardTitle> </CardTitle>
</div> </div>
<div className='grid gap-1 font-semibold text-muted-foreground'> <div className='grid gap-1 font-semibold text-muted-foreground'>
<CardDescription className='text-sm'>Imp. descuento</CardDescription> <CardDescription className='text-sm'>Imp. descuento</CardDescription>
<CardTitle className='flex items-baseline gap-1 text-3xl tabular-nums'> <CardTitle className='flex items-baseline text-2xl tabular-nums'>
{subtotal_price.amount / 100} {subtotal_price}
<span className='text-base font-medium tracking-normal'></span> <span className='ml-1 text-lg tracking-normal'></span>
</CardTitle> </CardTitle>
</div> </div>
</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 flex-1 h-16 grid-cols-2 gap-2 auto-rows-max'>
<div className='grid gap-1 font-medium text-muted-foreground'> <div className='grid gap-1 font-medium text-muted-foreground'>
<CardDescription className='text-sm'>IVA</CardDescription> <CardDescription className='text-sm'>IVA</CardDescription>
<CardTitle className='flex items-baseline gap-1 text-2xl tabular-nums'> <CardTitle className='flex items-baseline gap-1 text-xl tabular-nums'>
{discount.amount / 100} {discount}
<span className='text-base tracking-normal'>%</span> <span className='text-base tracking-normal'>%</span>
</CardTitle> </CardTitle>
</div> </div>
<div className='grid gap-1 font-semibold text-muted-foreground'> <div className='grid gap-1 font-semibold text-muted-foreground'>
<CardDescription className='text-sm'>Importe IVA</CardDescription> <CardDescription className='text-sm'>Importe IVA</CardDescription>
<CardTitle className='flex items-baseline gap-1 text-3xl tabular-nums'> <CardTitle className='flex items-baseline gap-1 text-2xl tabular-nums'>
{subtotal_price.amount / 100} {subtotal_price}
<span className='text-base font-medium tracking-normal'></span> <span className='text-base font-medium tracking-normal'></span>
</CardTitle> </CardTitle>
</div> </div>
@ -60,9 +64,9 @@ export const QuotePricesResume = () => {
<div className='grid flex-1 h-16 grid-cols-1 auto-rows-max'> <div className='grid flex-1 h-16 grid-cols-1 auto-rows-max'>
<div className='grid gap-0'> <div className='grid gap-0'>
<CardDescription className='text-sm font-semibold'>Importe total</CardDescription> <CardDescription className='text-sm font-semibold'>Importe total</CardDescription>
<CardTitle className='flex items-baseline gap-1 text-4xl tabular-nums'> <CardTitle className='flex items-baseline gap-1 text-3xl tabular-nums'>
{subtotal_price.amount / 100} {total_price}
<span className='text-base font-medium tracking-normal'></span> <span className='ml-1 text-lg tracking-normal'></span>
</CardTitle> </CardTitle>
</div> </div>
</div> </div>

View File

@ -24,6 +24,34 @@ import { useQuotes } from "./hooks";
type QuoteDataForm = IGetQuote_Response_DTO; 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 // eslint-disable-next-line @typescript-eslint/no-unused-vars
export const QuoteEdit = () => { export const QuoteEdit = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -111,6 +139,7 @@ export const QuoteEdit = () => {
mode: "onBlur", mode: "onBlur",
values: data, values: data,
defaultValues, defaultValues,
//shouldUnregister: true,
}); });
const { watch, getValues, setValue, formState } = form; const { watch, getValues, setValue, formState } = form;
@ -146,7 +175,7 @@ export const QuoteEdit = () => {
}); });
}; };
useEffect(() => { /*useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { unsubscribe } = watch((_, { name, type }) => { const { unsubscribe } = watch((_, { name, type }) => {
const value = getValues(); const value = getValues();
@ -183,7 +212,7 @@ export const QuoteEdit = () => {
}); });
// Recálculo completo // 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")) { 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()); setValue(`items.${index}.total_price`, itemTotals.totalPrice.toObject());
// Recálculo completo // 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(); 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) { if (isSubmitting) {
return <LoadingOverlay title='Guardando cotización' />; return <LoadingOverlay title='Guardando cotización' />;

View File

@ -1,52 +1,88 @@
/* https://github.com/mayank8aug/use-localization/blob/main/src/index.ts */ /* https://github.com/mayank8aug/use-localization/blob/main/src/index.ts */
import { useCallback, useMemo } from "react"; import { IMoney, IPercentage, IQuantity } from "@/lib/types";
import { LocaleToCurrencyTable, rtlLangsList } from "./utils"; import { useCallback } from "react";
import { useTranslation } from "react-i18next";
type UseLocalizationProps = { type UseLocalizationProps = {
locale: string; 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 { locale } = props;
const [lang, loc] = locale.split("-");
//const { i18n } = useTranslation();
// Obtener el idioma actual
// const currentLanguage = i18n.language;
const formatCurrency = useCallback( 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, { return new Intl.NumberFormat(locale, {
style: "currency", style: "currency",
currency: LocaleToCurrencyTable[locale], currency: currency_code,
}).format(value); currencyDisplay: "symbol",
maximumFractionDigits: scale,
}).format(amount === null ? 0 : amount);
}, },
[locale] [locale]
); );
const formatNumber = useCallback( const formatNumber = useCallback(
(value: number) => { (value: IMoney | IPercentage | IQuantity | null) => {
return new Intl.NumberFormat(locale).format(value); 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] [locale]
); );
const flag = useMemo( const formatPercentage = useCallback(
() => (value: IPercentage | null) => {
typeof String.fromCodePoint !== "undefined" if (value === null || value === undefined) {
? loc return "";
.toUpperCase() }
.replace(/./g, (char) => String.fromCodePoint(char.charCodeAt(0) + 127397))
: "", const { amount, scale } = value;
[loc]
return new Intl.NumberFormat(locale, {
style: "decimal",
minimumFractionDigits: scale,
}).format(amount === null ? 0 : amount);
},
[locale]
); );
return { return {
formatCurrency, formatCurrency,
formatNumber, formatNumber,
flag, formatPercentage,
isRTL: rtlLangsList.includes(lang),
}; };
}; };

View File

@ -1,9 +1,5 @@
import { import { IMoney_DTO, IPercentage_DTO, IQuantity_DTO } from "@shared/contexts";
IMoney_Response_DTO,
IPercentage_Response_DTO,
IQuantity_Response_DTO,
} from "@shared/contexts";
export interface IMoney extends IMoney_Response_DTO {} export interface IMoney extends IMoney_DTO {}
export interface IQuantity extends IQuantity_Response_DTO {} export interface IQuantity extends IQuantity_DTO {}
export interface IPercentage extends IPercentage_Response_DTO {} export interface IPercentage extends IPercentage_DTO {}

View File

@ -26,9 +26,9 @@ export const GetQuotePresenter: IGetQuotePresenter = {
validity: quote.validity.toString(), validity: quote.validity.toString(),
notes: quote.notes.toString(), notes: quote.notes.toString(),
subtotal_price: quote.subtotalPrice.toObject(), subtotal_price: quote.subtotalPrice.convertScale(2).toObject(),
discount: quote.discount.toObject(), discount: quote.discount.convertScale(2).toObject(),
total_price: quote.totalPrice.toObject(), total_price: quote.totalPrice.convertScale(2).toObject(),
items: quoteItemPresenter(quote.items, context), items: quoteItemPresenter(quote.items, context),
dealer_id: quote.dealerId.toString(), dealer_id: quote.dealerId.toString(),
@ -45,10 +45,10 @@ const quoteItemPresenter = (
? items.items.map((item: QuoteItem) => ({ ? items.items.map((item: QuoteItem) => ({
article_id: item.articleId.toString(), article_id: item.articleId.toString(),
description: item.description.toString(), description: item.description.toString(),
quantity: item.quantity.toObject(), quantity: item.quantity.convertScale(2).toObject(),
unit_price: item.unitPrice.toObject(), unit_price: item.unitPrice.convertScale(4).toObject(),
subtotal_price: item.subtotalPrice.toObject(), subtotal_price: item.subtotalPrice.convertScale(4).toObject(),
discount: item.discount.toObject(), discount: item.discount.convertScale(2).toObject(),
total_price: item.totalPrice.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 notes: CreationOptional<string>;
declare validity: CreationOptional<string>; declare validity: CreationOptional<string>;
declare subtotal_price: CreationOptional<number>; declare subtotal_price: CreationOptional<number | null>;
declare discount: 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 items: NonAttribute<QuoteItem_Model[]>;
declare dealer: NonAttribute<Dealer_Model>; declare dealer: NonAttribute<Dealer_Model>;