This commit is contained in:
David Arranz 2025-11-09 12:05:33 +01:00
parent 5a4b457384
commit 6be0ad686c
2 changed files with 110 additions and 109 deletions

View File

@ -1,6 +1,12 @@
import { formatCurrency } from "@erp/core"; import { formatCurrency } from "@erp/core";
import { FieldDescription, FieldGroup, FieldLegend, FieldSet, Separator } from '@repo/shadcn-ui/components'; import {
import { cn } from '@repo/shadcn-ui/lib/utils'; FieldDescription,
FieldGroup,
FieldLegend,
FieldSet,
Separator,
} from "@repo/shadcn-ui/components";
import { cn } from "@repo/shadcn-ui/lib/utils";
import { ReceiptIcon } from "lucide-react"; import { ReceiptIcon } from "lucide-react";
import { ComponentProps } from "react"; import { ComponentProps } from "react";
import { useFormContext, useWatch } from "react-hook-form"; import { useFormContext, useWatch } from "react-hook-form";
@ -20,46 +26,55 @@ export const InvoiceTotals = (props: ComponentProps<"fieldset">) => {
defaultValue: [], defaultValue: [],
}); });
const subtotal_amount = useWatch({
control,
name: "subtotal_amount",
defaultValue: 0,
});
return ( return (
<FieldSet {...props}> <FieldSet {...props}>
<FieldLegend className='hidden'> <FieldLegend className='hidden'>
<ReceiptIcon className='size-6 text-muted-foreground' />{t("form_groups.totals.title")} <ReceiptIcon className='size-6 text-muted-foreground' />
{t("form_groups.totals.title")}
</FieldLegend> </FieldLegend>
<FieldDescription className='hidden'>{t("form_groups.totals.description")}</FieldDescription> <FieldDescription className='hidden'>{t("form_groups.totals.description")}</FieldDescription>
<FieldGroup className='grid grid-cols-1 border rounded-lg bg-muted/10 p-4 gap-4'> <FieldGroup className='grid grid-cols-1 border rounded-lg bg-muted/10 p-4 gap-4'>
<div className='space-y-1.5'> <div className='space-y-1.5'>
{/* Sección: Subtotal y Descuentos */} {/* Sección: Subtotal y Descuentos */}
<div className="flex justify-between text-sm"> <div className='flex justify-between text-sm'>
<span className="text-muted-foreground">Subtotal sin descuento</span> <span className='text-muted-foreground'>Subtotal sin descuento</span>
<span className="font-medium tabular-nums text-muted-foreground"> <span className='font-medium tabular-nums text-muted-foreground'>
{formatCurrency(getValues('subtotal_amount'), 2, currency_code, language_code)} {formatCurrency(subtotal_amount, 2, currency_code, language_code)}
</span> </span>
</div> </div>
<div className="flex justify-between text-sm"> <div className='flex justify-between text-sm'>
<div className="flex items-center gap-3"> <div className='flex items-center gap-3'>
<span className="text-muted-foreground">Descuento global</span> <span className='text-muted-foreground'>Descuento global</span>
<PercentageInputField <PercentageInputField
control={control} control={control}
name={"discount_percentage"} name={"discount_percentage"}
readOnly={readOnly} readOnly={readOnly}
inputId={"header-discount-percentage"} inputId={"header-discount-percentage"}
showSuffix={true} showSuffix={true}
className={cn("w-20 text-right tabular-nums bg-background", "border-input border text-sm shadow-xs")} className={cn(
"w-20 text-right tabular-nums bg-background",
"border-input border text-sm shadow-xs"
)}
/> />
</div> </div>
<span className="font-medium text-destructive tabular-nums">-{formatCurrency(getValues("discount_amount"), 2, currency_code, language_code)}</span> <span className='font-medium text-destructive tabular-nums'>
-{formatCurrency(getValues("discount_amount"), 2, currency_code, language_code)}
</span>
</div> </div>
{/* Sección: Base Imponible */} {/* Sección: Base Imponible */}
<div className="flex justify-between text-sm"> <div className='flex justify-between text-sm'>
<span className="text-foreground">Base imponible</span> <span className='text-foreground'>Base imponible</span>
<span className="font-medium tabular-nums"> <span className='font-medium tabular-nums'>
{formatCurrency(getValues('taxable_amount'), 2, currency_code, language_code)} {formatCurrency(getValues("taxable_amount"), 2, currency_code, language_code)}
</span> </span>
</div> </div>
</div> </div>
@ -67,10 +82,8 @@ export const InvoiceTotals = (props: ComponentProps<"fieldset">) => {
<Separator /> <Separator />
{/* Sección: Impuestos */} {/* Sección: Impuestos */}
<div className="space-y-1.5"> <div className='space-y-1.5'>
<h3 <h3 className='text-xs font-semibold text-muted-foreground uppercase tracking-wide'>
className="text-xs font-semibold text-muted-foreground uppercase tracking-wide"
>
Impuestos y retenciones Impuestos y retenciones
</h3> </h3>
@ -88,9 +101,7 @@ export const InvoiceTotals = (props: ComponentProps<"fieldset">) => {
if (taxesInGroup?.length === 0) return null; if (taxesInGroup?.length === 0) return null;
return ( return (
<div key={`tax-group-${group}`} className='space-y-1.5 leading-3'>
<div key={`tax-group-${group}`} className="space-y-1.5 leading-3">
{taxesInGroup?.map((item) => { {taxesInGroup?.map((item) => {
const tax = taxCatalog.findByCode(item.tax_code).match( const tax = taxCatalog.findByCode(item.tax_code).match(
(t) => t, (t) => t,
@ -99,43 +110,36 @@ export const InvoiceTotals = (props: ComponentProps<"fieldset">) => {
return ( return (
<div <div
key={`${group}:${item.tax_code}`} key={`${group}:${item.tax_code}`}
className="flex items-center justify-between text-sm" className='flex items-center justify-between text-sm'
> >
<span className="text-muted-foreground text-sm">{tax?.name}</span> <span className='text-muted-foreground text-sm'>{tax?.name}</span>
<span className="font-medium tabular-nums text-sm text-muted-foreground"> <span className='font-medium tabular-nums text-sm text-muted-foreground'>
{formatCurrency( {formatCurrency(item.taxes_amount, 2, currency_code, language_code)}
item.taxes_amount,
2,
currency_code,
language_code
)}
</span> </span>
</div> </div>
); );
})} })}
</div> </div>
); );
})} })}
<div className="flex justify-between text-sm mt-3"> <div className='flex justify-between text-sm mt-3'>
<span className="text-foreground">Total de impuestos</span> <span className='text-foreground'>Total de impuestos</span>
<span className="font-medium tabular-nums"> <span className='font-medium tabular-nums'>
{formatCurrency(getValues('taxes_amount'), 2, currency_code, language_code)} {formatCurrency(getValues("taxes_amount"), 2, currency_code, language_code)}
</span> </span>
</div> </div>
</div> </div>
<Separator /> <Separator />
<div className="flex justify-between text-sm "> <div className='flex justify-between text-sm '>
<span className="font-bold text-foreground">Total de la factura</span> <span className='font-bold text-foreground'>Total de la factura</span>
<span className="font-bold tabular-nums"> <span className='font-bold tabular-nums'>
{formatCurrency(getValues('total_amount'), 2, currency_code, language_code)} {formatCurrency(getValues("total_amount"), 2, currency_code, language_code)}
</span> </span>
</div> </div>
</FieldGroup> </FieldGroup>
</FieldSet > </FieldSet>
); );
}; };

View File

@ -1,49 +1,47 @@
import { DataTable, useWithRowSelection } from '@repo/rdx-ui/components'; import { DataTable, useWithRowSelection } from "@repo/rdx-ui/components";
import { useMemo } from 'react'; import { useMemo } from "react";
import { useFieldArray, useFormContext } from "react-hook-form"; import { useFieldArray, useFormContext } from "react-hook-form";
import { useInvoiceContext } from '../../../context'; import { useInvoiceContext } from "../../../context";
import { useInvoiceAutoRecalc } from '../../../hooks'; import { useInvoiceAutoRecalc } from "../../../hooks";
import { useTranslation } from '../../../i18n'; import { useTranslation } from "../../../i18n";
import { InvoiceFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas'; import { defaultCustomerInvoiceItemFormData, InvoiceFormData } from "../../../schemas";
import { debugIdCol } from './debug-id-col'; import { debugIdCol } from "./debug-id-col";
import { ItemRowEditor } from './item-row-editor'; import { ItemRowEditor } from "./item-row-editor";
import { useItemsColumns } from './use-items-columns'; import { useItemsColumns } from "./use-items-columns";
const createEmptyItem = () => defaultCustomerInvoiceItemFormData; const createEmptyItem = () => defaultCustomerInvoiceItemFormData;
export const ItemsEditor = () => { export const ItemsEditor = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const context = useInvoiceContext(); const context = useInvoiceContext();
const form = useFormContext<InvoiceFormData>(); const form = useFormContext<InvoiceFormData>();
const { control, getValues } = form; const { control, getValues } = form;
useInvoiceAutoRecalc(form, context); useInvoiceAutoRecalc(form, context);
const { fields, append, remove, move, insert, update } = useFieldArray({ const { fields, append, remove, move, insert, update } = useFieldArray({
control, control,
name: "items", name: "items",
}); });
const baseColumns = useWithRowSelection(useItemsColumns(), true); const baseColumns = useWithRowSelection(useItemsColumns(), true);
const columns = useMemo( const columns = useMemo(() => [...baseColumns, debugIdCol], [baseColumns]);
() => [...baseColumns, debugIdCol],
[baseColumns]
);
return ( return (
<div className="space-y-0"> <div className='space-y-0'>
<DataTable columns={columns as any} data={fields} <DataTable
meta={{ columns={columns as any}
tableOps: { data={fields}
onAdd: () => append({ ...createEmptyItem() }), meta={{
//appendItem: (item: any) => append(item), tableOps: {
}, onAdd: () => append({ ...createEmptyItem() }),
rowOps: { //appendItem: (item: any) => append(item),
remove: (i: number) => remove(i), },
move: (from: number, to: number) => move(from, to), rowOps: {
//insertItem: (index: number, item: any) => insert(index, item), remove: (i: number) => remove(i),
/*duplicateItems: (indexes: number[], table: Table<InvoiceFormData>) => { move: (from: number, to: number) => move(from, to),
//insertItem: (index: number, item: any) => insert(index, item),
/*duplicateItems: (indexes: number[], table: Table<InvoiceFormData>) => {
const items = getValues("items") || []; const items = getValues("items") || [];
// duplicate in descending order to keep indexes stable // duplicate in descending order to keep indexes stable
[...indexes].sort((a, b) => b - a).forEach(i => { [...indexes].sort((a, b) => b - a).forEach(i => {
@ -54,34 +52,33 @@ export const ItemsEditor = () => {
} }
}); });
},*/ },*/
/*deleteItems: (indexes: number[]) => { /*deleteItems: (indexes: number[]) => {
// remove in descending order to avoid shifting issues // remove in descending order to avoid shifting issues
[...indexes].sort((a, b) => b - a).forEach(i => remove(i)); [...indexes].sort((a, b) => b - a).forEach(i => remove(i));
},*/ },*/
//updateItem: (index: number, item: any) => update(index, item), //updateItem: (index: number, item: any) => update(index, item),
}, },
bulkOps: { bulkOps: {
duplicateSelected: (indexes, table) => { duplicateSelected: (indexes, table) => {
const originalData = indexes.map((i) => { const originalData = indexes.map((i) => {
const { id, ...original } = table.getRowModel().rows[i].original; const { id, ...original } = table.getRowModel().rows[i].original;
return original; return original;
}); });
insert(indexes[indexes.length - 1] + 1, originalData, { shouldFocus: true });
table.resetRowSelection();
},
removeSelected: (indexes) => indexes.sort((a, b) => b - a).forEach(remove),
moveSelectedUp: (indexes) => indexes.forEach((i) => move(i, i - 1)),
moveSelectedDown: (indexes) => [...indexes].reverse().forEach((i) => move(i, i + 1)),
}
}}
enableRowSelection
enablePagination={false}
pageSize={999}
readOnly={false}
EditorComponent={ItemRowEditor}
/>
</div >
);
}
insert(indexes[indexes.length - 1] + 1, originalData, { shouldFocus: true });
table.resetRowSelection();
},
removeSelected: (indexes) => indexes.sort((a, b) => b - a).forEach(remove),
moveSelectedUp: (indexes) => indexes.forEach((i) => move(i, i - 1)),
moveSelectedDown: (indexes) => [...indexes].reverse().forEach((i) => move(i, i + 1)),
},
}}
enableRowSelection
enablePagination={false}
pageSize={999}
readOnly={false}
EditorComponent={ItemRowEditor}
/>
</div>
);
};