Uecko_ERP/modules/customer-invoices/src/web/components/editor/invoice-totals.tsx

146 lines
5.3 KiB
TypeScript
Raw Normal View History

2025-10-14 13:46:57 +00:00
import { formatCurrency } from "@erp/core";
2025-11-09 11:05:33 +00:00
import {
FieldDescription,
FieldGroup,
FieldLegend,
FieldSet,
Separator,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
2025-10-14 13:46:57 +00:00
import { ReceiptIcon } from "lucide-react";
import { ComponentProps } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { useInvoiceContext } from "../../context";
2025-09-29 18:22:59 +00:00
import { useTranslation } from "../../i18n";
2025-10-12 10:43:06 +00:00
import { InvoiceFormData } from "../../schemas";
2025-10-14 13:46:57 +00:00
import { PercentageInputField } from "./items/percentage-input-field";
2025-09-29 18:22:59 +00:00
2025-10-12 10:43:06 +00:00
export const InvoiceTotals = (props: ComponentProps<"fieldset">) => {
2025-09-29 18:22:59 +00:00
const { t } = useTranslation();
2025-10-12 10:43:06 +00:00
const { control, getValues } = useFormContext<InvoiceFormData>();
2025-10-14 13:46:57 +00:00
const { currency_code, language_code, readOnly, taxCatalog } = useInvoiceContext();
const displayTaxes = useWatch({
control,
name: "taxes",
defaultValue: [],
});
2025-11-09 11:05:33 +00:00
const subtotal_amount = useWatch({
control,
name: "subtotal_amount",
defaultValue: 0,
});
2025-09-29 18:22:59 +00:00
return (
2025-10-14 17:57:02 +00:00
<FieldSet {...props}>
2025-10-19 19:04:16 +00:00
<FieldLegend className='hidden'>
2025-11-09 11:05:33 +00:00
<ReceiptIcon className='size-6 text-muted-foreground' />
{t("form_groups.totals.title")}
2025-10-14 17:57:02 +00:00
</FieldLegend>
2025-09-29 18:22:59 +00:00
2025-10-19 19:04:16 +00:00
<FieldDescription className='hidden'>{t("form_groups.totals.description")}</FieldDescription>
2025-10-22 15:47:59 +00:00
<FieldGroup className='grid grid-cols-1 border rounded-lg bg-muted/10 p-4 gap-4'>
2025-10-19 19:04:16 +00:00
<div className='space-y-1.5'>
{/* Sección: Subtotal y Descuentos */}
2025-11-09 11:05:33 +00:00
<div className='flex justify-between text-sm'>
<span className='text-muted-foreground'>Subtotal sin descuento</span>
<span className='font-medium tabular-nums text-muted-foreground'>
{formatCurrency(subtotal_amount, 2, currency_code, language_code)}
2025-10-19 19:04:16 +00:00
</span>
</div>
2025-10-14 13:46:57 +00:00
2025-11-09 11:05:33 +00:00
<div className='flex justify-between text-sm'>
<div className='flex items-center gap-3'>
<span className='text-muted-foreground'>Descuento global</span>
2025-10-19 19:04:16 +00:00
<PercentageInputField
control={control}
name={"discount_percentage"}
readOnly={readOnly}
inputId={"header-discount-percentage"}
showSuffix={true}
2025-11-09 11:05:33 +00:00
className={cn(
"w-20 text-right tabular-nums bg-background",
"border-input border text-sm shadow-xs"
)}
2025-10-19 19:04:16 +00:00
/>
2025-09-29 18:22:59 +00:00
</div>
2025-11-09 11:05:33 +00:00
<span className='font-medium text-destructive tabular-nums'>
-{formatCurrency(getValues("discount_amount"), 2, currency_code, language_code)}
</span>
2025-09-29 18:22:59 +00:00
</div>
2025-10-14 13:46:57 +00:00
2025-10-19 19:04:16 +00:00
{/* Sección: Base Imponible */}
2025-11-09 11:05:33 +00:00
<div className='flex justify-between text-sm'>
<span className='text-foreground'>Base imponible</span>
<span className='font-medium tabular-nums'>
{formatCurrency(getValues("taxable_amount"), 2, currency_code, language_code)}
2025-09-29 18:22:59 +00:00
</span>
</div>
2025-10-14 13:46:57 +00:00
</div>
2025-09-29 18:22:59 +00:00
2025-10-14 13:46:57 +00:00
<Separator />
2025-09-29 18:22:59 +00:00
2025-10-14 13:46:57 +00:00
{/* Sección: Impuestos */}
2025-11-09 11:05:33 +00:00
<div className='space-y-1.5'>
<h3 className='text-xs font-semibold text-muted-foreground uppercase tracking-wide'>
2025-10-14 13:46:57 +00:00
Impuestos y retenciones
</h3>
{taxCatalog.groups().map((group) => {
// Filtra impuestos de ese grupo
const taxesInGroup = displayTaxes?.filter((item) => {
const tax = taxCatalog.findByCode(item.tax_code).match(
(t) => t,
() => undefined
);
return tax?.group === group;
});
2025-09-29 18:22:59 +00:00
2025-10-14 13:46:57 +00:00
// Si el grupo no tiene impuestos, no renderiza nada
if (taxesInGroup?.length === 0) return null;
return (
2025-11-09 11:05:33 +00:00
<div key={`tax-group-${group}`} className='space-y-1.5 leading-3'>
2025-10-14 13:46:57 +00:00
{taxesInGroup?.map((item) => {
const tax = taxCatalog.findByCode(item.tax_code).match(
(t) => t,
() => undefined
);
return (
<div
key={`${group}:${item.tax_code}`}
2025-11-09 11:05:33 +00:00
className='flex items-center justify-between text-sm'
2025-10-14 13:46:57 +00:00
>
2025-11-09 11:05:33 +00:00
<span className='text-muted-foreground text-sm'>{tax?.name}</span>
<span className='font-medium tabular-nums text-sm text-muted-foreground'>
{formatCurrency(item.taxes_amount, 2, currency_code, language_code)}
2025-10-14 13:46:57 +00:00
</span>
</div>
);
})}
</div>
);
})}
2025-11-09 11:05:33 +00:00
<div className='flex justify-between text-sm mt-3'>
<span className='text-foreground'>Total de impuestos</span>
<span className='font-medium tabular-nums'>
{formatCurrency(getValues("taxes_amount"), 2, currency_code, language_code)}
2025-10-19 19:04:16 +00:00
</span>
2025-09-29 18:22:59 +00:00
</div>
2025-10-14 13:46:57 +00:00
</div>
2025-09-29 18:22:59 +00:00
2025-10-19 19:04:16 +00:00
<Separator />
2025-11-09 11:05:33 +00:00
<div className='flex justify-between text-sm '>
<span className='font-bold text-foreground'>Total de la factura</span>
<span className='font-bold tabular-nums'>
{formatCurrency(getValues("total_amount"), 2, currency_code, language_code)}
2025-10-19 19:04:16 +00:00
</span>
2025-09-29 18:22:59 +00:00
</div>
</FieldGroup>
2025-11-09 11:05:33 +00:00
</FieldSet>
2025-09-29 18:22:59 +00:00
);
};