diff --git a/modules/core/src/web/hooks/use-money.ts b/modules/core/src/web/hooks/use-money.ts
index 924562c1..c97e9733 100644
--- a/modules/core/src/web/hooks/use-money.ts
+++ b/modules/core/src/web/hooks/use-money.ts
@@ -11,6 +11,8 @@ import {
} from "../../common/helpers";
import { useTranslation } from "../i18n";
+export type { Currency };
+
// --- Utils locales (edición texto → número) ---
// Quita símbolos de moneda/letras, conserva dígitos, signo y , .
diff --git a/modules/customer-invoices/src/web/components/editor/items/hover-card-total-summary.tsx b/modules/customer-invoices/src/web/components/editor/items/hover-card-total-summary.tsx
index bdf108a9..31b78921 100644
--- a/modules/customer-invoices/src/web/components/editor/items/hover-card-total-summary.tsx
+++ b/modules/customer-invoices/src/web/components/editor/items/hover-card-total-summary.tsx
@@ -1,31 +1,24 @@
-import { MoneyDTO, PercentageDTO, QuantityDTO } from '@erp/core';
import { useMoney } from '@erp/core/hooks';
+import { InvoiceItemTotals } from '@erp/customer-invoices/web/hooks/calcs';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger,
HoverCard, HoverCardContent, HoverCardTrigger
} from "@repo/shadcn-ui/components";
import { PropsWithChildren } from 'react';
-import { useInvoiceItemSummary } from '../../../hooks';
import { useTranslation } from "../../../i18n";
export type HoverCardTotalsSummaryProps = PropsWithChildren & {
- data: {
- quantity: QuantityDTO | null | undefined;
- unit_amount: MoneyDTO | null | undefined;
- discount_percentage?: PercentageDTO | null;
- tax_codes?: string[] | null;
- }
+ totals: InvoiceItemTotals
}
export const HoverCardTotalsSummary = ({
children,
- data
+ totals
}: HoverCardTotalsSummaryProps) => {
const { t } = useTranslation();
const { formatCurrency } = useMoney();
- const summary = useInvoiceItemSummary(data);
const SummaryBlock = () => (
@@ -33,16 +26,16 @@ export const HoverCardTotalsSummary = ({
{t("components.hover_card_totals_summary.fields.subtotal_amount")}:
- {formatCurrency(summary.subtotal)}
+ {formatCurrency(totals.subtotal)}
- {Number(data.discount_percentage?.value ?? 0) > 0 && (
+ {(totals.discountPercent ?? 0) > 0 && (
- {t("components.hover_card_totals_summary.fields.discount_percentage")} ({data.discount_percentage?.value ?? 0}%):
+ {t("components.hover_card_totals_summary.fields.discount_percentage")} ({totals.discountPercent ?? 0}%):
- -{formatCurrency(summary.discountAmount)}
+ -{formatCurrency(totals.discountAmount)}
)}
@@ -50,20 +43,20 @@ export const HoverCardTotalsSummary = ({
{t("components.hover_card_totals_summary.fields.taxable_amount")}:
- {formatCurrency(summary.baseAmount)}
+ {formatCurrency(totals.taxableBase)}
- {summary.taxesBreakdown.map((tax) => (
+ {/*totals.taxesBreakdown.map((tax) => (
{tax.label}:
{formatCurrency(tax.amount)}
- ))}
+ ))*/}
{t("components.hover_card_totals_summary.fields.total_amount")}:
- {formatCurrency(summary.total)}
+ {formatCurrency(totals.total)}
)
diff --git a/modules/customer-invoices/src/web/components/editor/items/items-editor-toolbar.tsx b/modules/customer-invoices/src/web/components/editor/items/items-editor-toolbar.tsx
index cd0e057a..8c2e6480 100644
--- a/modules/customer-invoices/src/web/components/editor/items/items-editor-toolbar.tsx
+++ b/modules/customer-invoices/src/web/components/editor/items/items-editor-toolbar.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from '../../../i18n';
export const ItemsEditorToolbar = ({
readOnly,
- selectedIdx,
+ selectedIndexes,
onAdd,
onDuplicate,
onMoveUp,
@@ -12,7 +12,7 @@ export const ItemsEditorToolbar = ({
onRemove,
}: {
readOnly: boolean;
- selectedIdx: number[];
+ selectedIndexes: number[];
onAdd?: () => void;
onDuplicate?: () => void;
onMoveUp?: () => void;
@@ -20,7 +20,7 @@ export const ItemsEditorToolbar = ({
onRemove?: () => void;
}) => {
const { t } = useTranslation();
- const hasSel = selectedIdx.length > 0;
+ const hasSel = selectedIndexes.length > 0;
return (
diff --git a/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx b/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx
index 10ba25d4..23231c17 100644
--- a/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx
+++ b/modules/customer-invoices/src/web/components/editor/items/items-editor.tsx
@@ -1,9 +1,9 @@
-import { SpainTaxCatalogProvider } from '@erp/core';
+import { useRowSelection } from '@repo/rdx-ui/hooks';
import { Button, Checkbox, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
import { ArrowDown, ArrowUp, CopyIcon, Trash2 } from "lucide-react";
import * as React from "react";
import { Controller, useFormContext } from "react-hook-form";
-import { useItemsTableNavigation } from '../../../hooks';
+import { useCalcInvoiceItemTotals, useItemsTableNavigation } from '../../../hooks';
import { useTranslation } from '../../../i18n';
import { CustomerInvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
@@ -22,16 +22,10 @@ interface ItemsEditorProps {
const createEmptyItem = () => defaultCustomerInvoiceItemFormData;
-function getSelectAllState(totalRows: number, selectedCount: number): boolean | 'indeterminate' {
- if (totalRows === 0 || selectedCount === 0) return false;
- if (selectedCount === totalRows) return true;
- return 'indeterminate';
-}
-
export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEditorProps) => {
const { t } = useTranslation();
const form = useFormContext();
- const taxCatalog = React.useMemo(() => SpainTaxCatalogProvider(), []);
+
const tableNav = useItemsTableNavigation(form, {
name: "items",
@@ -39,11 +33,17 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
firstEditableField: "description",
});
+ const {
+ selectedRows,
+ selectedIndexes,
+ selectAllState,
+ toggleRow,
+ setSelectAll,
+ clearSelection,
+ } = useRowSelection(tableNav.fa.fields.length);
+
const { control, watch } = form;
- const [selection, setSelection] = React.useState>(new Set());
- const selectedIdx = React.useMemo(() => [...selection].sort((a, b) => a - b), [selection]);
- const resetSelection = () => setSelection(new Set());
// Emitir cambios a quien consuma el componente
@@ -52,29 +52,21 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
return () => sub.unsubscribe();
}, [watch, onChange]);
- const toggleSel = (i: number) =>
- setSelection((prev) => {
- const next = new Set(prev);
- next.has(i) ? next.delete(i) : next.add(i);
- return next;
- });
-
return (
{/* Toolbar selección múltiple */}
tableNav.addEmpty(true)}
- onDuplicate={() => selectedIdx.forEach((i) => tableNav.duplicate(i))}
- onMoveUp={() => selectedIdx.forEach((i) => tableNav.moveUp(i))}
- onMoveDown={() => [...selectedIdx].reverse().forEach((i) => tableNav.moveDown(i))}
+ onDuplicate={() => selectedIndexes.forEach((i) => tableNav.duplicate(i))}
+ onMoveUp={() => selectedIndexes.forEach((i) => tableNav.moveUp(i))}
+ onMoveDown={() => [...selectedIndexes].reverse().forEach((i) => tableNav.moveDown(i))}
onRemove={() => {
- [...selectedIdx].reverse().forEach((i) => tableNav.remove(i));
- resetSelection();
- }}
- />
+ [...selectedIndexes].reverse().forEach((i) => tableNav.remove(i));
+ clearSelection();
+ }} />
@@ -96,17 +88,9 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
{
- const shouldSelectAll = next !== false;
- if (shouldSelectAll) {
- setSelection(new Set(tableNav.fa.fields.map((_, i) => i)));
- } else {
- resetSelection();
- }
- }}
+ onCheckedChange={(checked) => setSelectAll(checked)}
/>
@@ -122,11 +106,19 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
{tableNav.fa.fields.map((f, rowIndex) => {
- //const comp = calculateItemAmounts(f);
- //console.log(comp);
+
const isFirst = rowIndex === 0;
const isLast = rowIndex === tableNav.fa.fields.length - 1;
+ const item = form.watch(`items.${rowIndex}`);
+ const totals = useCalcInvoiceItemTotals(item);
+
+ // sincronizar con react-hook-form
+ React.useEffect(() => {
+ form.setValue(`items.${rowIndex}.total_amount`, totals.totalDTO, { shouldDirty: true });
+ }, [totals.totalDTO, form, rowIndex]);
+
+
return (
{/* selección */}
@@ -134,11 +126,11 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
toggleSel(rowIndex)}
+ checked={selectedRows.has(rowIndex)}
disabled={readOnly}
- />
+ onCheckedChange={() => toggleRow(rowIndex)}
+ />
+
{/* # */}
@@ -222,15 +214,13 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
{/* total (solo lectura) */}
-
+
@@ -298,7 +288,7 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
tableNav.addEmpty(true)}
/>
diff --git a/modules/customer-invoices/src/web/components/editor/items/numeric-input.tsx b/modules/customer-invoices/src/web/components/editor/items/numeric-input.tsx
deleted file mode 100644
index 8eab8e1a..00000000
--- a/modules/customer-invoices/src/web/components/editor/items/numeric-input.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import * as React from "react";
-import { parseNum } from './types.d';
-
-type NumericInputProps = React.InputHTMLAttributes & {
- value?: number;
- onValueChange: (n: number | undefined) => void;
- maxDecimals: 2 | 4;
- readOnly?: boolean;
-};
-export function NumericInput({ value, onValueChange, maxDecimals, readOnly, ...rest }: NumericInputProps) {
- const [raw, setRaw] = React.useState(() => value ?? value === 0 ? String(value) : "");
- React.useEffect(() => { if (document.activeElement !== ref.current) setRaw(value ?? value === 0 ? String(value) : ""); }, [value]);
- const ref = React.useRef(null);
-
- return (
- setRaw(e.target.value)}
- onBlur={() => {
- const n = parseNum(raw);
- if (n === undefined) { onValueChange(undefined); setRaw(""); return; }
- const factor = maxDecimals === 4 ? 1e4 : 1e2;
- const rounded = Math.round(n * factor) / factor;
- onValueChange(rounded);
- // ocultar ceros a la derecha
- setRaw(Intl.NumberFormat(undefined, { maximumFractionDigits: maxDecimals }).format(rounded));
- }}
- onKeyDown={(e) => {
- if ((e.key === "ArrowUp" || e.key === "ArrowDown") && !readOnly) {
- e.preventDefault();
- const n = parseNum(raw) ?? 0;
- const delta = e.shiftKey ? 1 : 0.1;
- const next = e.key === "ArrowUp" ? n + delta : n - delta;
- onValueChange(Number(next.toFixed(maxDecimals)));
- setRaw(String(Number(next.toFixed(maxDecimals))));
- }
- }}
- {...rest}
- />
- );
-}
diff --git a/modules/customer-invoices/src/web/components/editor/items/table-view.tsx b/modules/customer-invoices/src/web/components/editor/items/table-view.tsx
index 7db506ec..18eb5669 100644
--- a/modules/customer-invoices/src/web/components/editor/items/table-view.tsx
+++ b/modules/customer-invoices/src/web/components/editor/items/table-view.tsx
@@ -19,7 +19,6 @@ import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Plus, TrashIcon } from "lucid
import { useMoney } from '@erp/core/hooks';
import { useEffect, useState } from 'react';
import { useFormContext } from 'react-hook-form';
-import { useCalculateItemAmounts } from '../../../hooks';
import { useTranslation } from '../../../i18n';
import { CustomerInvoiceItemFormData } from '../../../schemas';
import { HoverCardTotalsSummary } from './hover-card-total-summary';
@@ -31,7 +30,6 @@ export const TableView = ({ items, actions }: TableViewProps) => {
const { t } = useTranslation();
const { control } = useFormContext();
const { format } = useMoney();
- const calculateItemAmounts = useCalculateItemAmounts();
const [lines, setLines] = useState(items);
useEffect(() => {
diff --git a/modules/customer-invoices/src/web/hooks/calcs/index.ts b/modules/customer-invoices/src/web/hooks/calcs/index.ts
new file mode 100644
index 00000000..95ae5e4e
--- /dev/null
+++ b/modules/customer-invoices/src/web/hooks/calcs/index.ts
@@ -0,0 +1,2 @@
+export * from "./use-calc-invoice-items-totals";
+export * from "./use-calc-invoice-totals";
diff --git a/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-items-totals.ts b/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-items-totals.ts
new file mode 100644
index 00000000..7e3b4c9f
--- /dev/null
+++ b/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-items-totals.ts
@@ -0,0 +1,77 @@
+import { MoneyDTO } from "@erp/core";
+import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks";
+import { useMemo } from "react";
+import { CustomerInvoiceItemFormData } from "../../schemas";
+
+/**
+ * Calcula totales derivados de un ítem de factura
+ */
+
+export type InvoiceItemTotals = Readonly<{
+ // valores base ya normalizados a number
+ quantity: number;
+ unitAmount: number;
+ discountPercent: number;
+
+ // desgloses numéricos
+ subtotal: number; // qty * unit
+ discountAmount: number; // subtotal * (discountPercent/100)
+ taxableBase: number; // subtotal - discountAmount
+ taxes: number; // por ahora 0 (o calcula según tax_codes si lo necesitas)
+ total: number; // taxableBase + taxes
+
+ // equivalentes en MoneyDTO (misma divisa/escala que useMoney)
+ subtotalDTO: MoneyDTO;
+ discountAmountDTO: MoneyDTO;
+ taxableBaseDTO: MoneyDTO;
+ taxesDTO: MoneyDTO;
+ totalDTO: MoneyDTO;
+}>;
+/**
+ * Calcula totales derivados de una línea de factura usando tus hooks de Money/Quantity/Percentage.
+ */
+export function useCalcInvoiceItemTotals(item?: CustomerInvoiceItemFormData): InvoiceItemTotals {
+ const moneyHelper = useMoney();
+ const qtyHelper = useQuantity();
+ const pctHelper = usePercentage();
+
+ return useMemo(() => {
+ // valores base
+ const quantity = item ? qtyHelper.toNumber(item.quantity) : 0;
+ const unitAmount = item ? moneyHelper.toNumber(item.unit_amount) : 0;
+ const discountPercent = item ? pctHelper.toNumber(item.discount_percentage) : 0;
+
+ // cálculos
+ const subtotal = quantity * unitAmount;
+ const discountAmount = subtotal * (discountPercent / 100);
+ const taxableBase = subtotal - discountAmount;
+
+ // impuestos (ajústalo si quieres aplicar tax_codes)
+ const taxes = 0;
+
+ const total = taxableBase + taxes;
+
+ // DTOs
+ const subtotalDTO = moneyHelper.fromNumber(subtotal);
+ const discountAmountDTO = moneyHelper.fromNumber(discountAmount);
+ const taxableBaseDTO = moneyHelper.fromNumber(taxableBase);
+ const taxesDTO = moneyHelper.fromNumber(taxes);
+ const totalDTO = moneyHelper.fromNumber(total);
+
+ return {
+ quantity,
+ unitAmount,
+ discountPercent,
+ subtotal,
+ discountAmount,
+ taxableBase,
+ taxes,
+ total,
+ subtotalDTO,
+ discountAmountDTO,
+ taxableBaseDTO,
+ taxesDTO,
+ totalDTO,
+ };
+ }, [item, moneyHelper, qtyHelper, pctHelper]);
+}
diff --git a/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-totals.ts b/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-totals.ts
new file mode 100644
index 00000000..70759c5e
--- /dev/null
+++ b/modules/customer-invoices/src/web/hooks/calcs/use-calc-invoice-totals.ts
@@ -0,0 +1,89 @@
+import { MoneyDTO } from "@erp/core";
+import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks";
+import { useMemo } from "react";
+import { CustomerInvoiceItemFormData } from "../../schemas";
+
+export type InvoiceTotals = Readonly<{
+ subtotal: number;
+ discountTotal: number;
+ taxableBase: number;
+ taxes: number;
+ total: number;
+
+ subtotalDTO: MoneyDTO;
+ discountTotalDTO: MoneyDTO;
+ taxableBaseDTO: MoneyDTO;
+ taxesDTO: MoneyDTO;
+ totalDTO: MoneyDTO;
+
+ // número de líneas válidas consideradas
+ itemCount: number;
+}>;
+
+/**
+ * Calcula los totales generales de la factura a partir de sus líneas.
+ */
+export function useCalcInvoiceTotals(
+ items: CustomerInvoiceItemFormData[] | undefined
+): InvoiceTotals {
+ const money = useMoney();
+ const qty = useQuantity();
+ const pct = usePercentage();
+
+ return useMemo(() => {
+ if (!items?.length) {
+ const zero = money.fromNumber(0);
+ return {
+ subtotal: 0,
+ discountTotal: 0,
+ taxableBase: 0,
+ taxes: 0,
+ total: 0,
+ subtotalDTO: zero,
+ discountTotalDTO: zero,
+ taxableBaseDTO: zero,
+ taxesDTO: zero,
+ totalDTO: zero,
+ itemCount: 0,
+ };
+ }
+
+ let subtotal = 0;
+ let discountTotal = 0;
+ let taxableBase = 0;
+ let taxes = 0;
+ let total = 0;
+
+ for (const item of items) {
+ const quantity = qty.toNumber(item.quantity);
+ const unit = money.toNumber(item.unit_amount);
+ const discountPct = pct.toNumber(item.discount_percentage);
+
+ const lineSubtotal = quantity * unit;
+ const lineDiscount = lineSubtotal * (discountPct / 100);
+ const lineTaxable = lineSubtotal - lineDiscount;
+ const lineTaxes = 0; // ← ajusta si aplicas IVA o impuestos reales
+ const lineTotal = lineTaxable + lineTaxes;
+
+ subtotal += lineSubtotal;
+ discountTotal += lineDiscount;
+ taxableBase += lineTaxable;
+ taxes += lineTaxes;
+ total += lineTotal;
+ }
+
+ return {
+ subtotal,
+ discountTotal,
+ taxableBase,
+ taxes,
+ total,
+ subtotalDTO: money.fromNumber(subtotal),
+ discountTotalDTO: money.fromNumber(discountTotal),
+ taxableBaseDTO: money.fromNumber(taxableBase),
+ taxesDTO: money.fromNumber(taxes),
+ totalDTO: money.fromNumber(total),
+ itemCount: items.length,
+ };
+ }, [items, money, qty, pct]);
+}
diff --git a/modules/customer-invoices/src/web/hooks/use-customer-invoice-item-summary.ts b/modules/customer-invoices/src/web/hooks/calcs/use-customer-invoice-item-summary.ts
similarity index 100%
rename from modules/customer-invoices/src/web/hooks/use-customer-invoice-item-summary.ts
rename to modules/customer-invoices/src/web/hooks/calcs/use-customer-invoice-item-summary.ts
diff --git a/modules/customer-invoices/src/web/hooks/index.ts b/modules/customer-invoices/src/web/hooks/index.ts
index 1d4b4029..63bc383a 100644
--- a/modules/customer-invoices/src/web/hooks/index.ts
+++ b/modules/customer-invoices/src/web/hooks/index.ts
@@ -1,6 +1,5 @@
-export * from "./use-calculate-item-amounts";
+export * from "./calcs";
export * from "./use-create-customer-invoice-mutation";
-export * from "./use-customer-invoice-item-summary";
export * from "./use-customer-invoice-query";
export * from "./use-customer-invoices-context";
export * from "./use-customer-invoices-query";
diff --git a/modules/customer-invoices/src/web/hooks/use-calculate-item-amounts.ts b/modules/customer-invoices/src/web/hooks/use-calculate-item-amounts.ts
deleted file mode 100644
index 350fd3c5..00000000
--- a/modules/customer-invoices/src/web/hooks/use-calculate-item-amounts.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { MoneyDTO, PercentageDTO, TaxCatalogProvider } from "@erp/core";
-import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks";
-import * as React from "react";
-import { CustomerInvoiceItem } from "../schemas";
-
-/**
- * Recalcula todos los importes de una línea usando los hooks de escala.
- */
-
-type UseCalculateItemAmountsParams = {
- locale: string;
- currencyCode: string;
- keepNullWhenEmpty?: boolean; // Mantener todos los importes a null cuando la línea está “vacía” (qty+unit vacíos)
- taxCatalog: TaxCatalogProvider; // Catálogo de impuestos (inyectable para test)
-};
-
-export function useCalculateItemAmounts(params: UseCalculateItemAmountsParams) {
- const { locale, currencyCode, taxCatalog, keepNullWhenEmpty } = params;
-
- const {
- add,
- sub,
- multiply,
- percentage: moneyPct,
- fromNumber,
- toNumber,
- isEmptyMoneyDTO,
- fallbackCurrency,
- } = useMoney();
- const { toNumber: qtyToNumber } = useQuantity();
- const { toNumber: pctToNumber } = usePercentage();
-
- // Crea un MoneyDTO "cero" con la misma divisa/escala que unit_amount
- const zeroOf = React.useCallback(
- (unit?: MoneyDTO | null): MoneyDTO => {
- const cur = unit?.currency_code ?? fallbackCurrency;
- const sc = Number(unit?.scale ?? 2);
- return fromNumber(0, cur as any, sc);
- },
- [fromNumber, fallbackCurrency]
- );
-
- const emptyAmountDTO = React.useMemo(
- () => ({
- value: "",
- scale: "4",
- currency_code: currencyCode,
- }),
- []
- );
-
- return React.useCallback(
- (item: CustomerInvoiceItem): CustomerInvoiceItem => {
- const qty = qtyToNumber(item.quantity); // 0 si vacío
- const unit = item.unit_amount && !isEmptyMoneyDTO(item.unit_amount) ? item.unit_amount : null;
- const zero = zeroOf(unit ?? undefined);
-
- // Línea “vacía”: mantener null si se pide y no hay datos
- const isEmptyLine = qty === 0 && (!unit || toNumber(unit) === 0);
- if (isEmptyLine && keepNullWhenEmpty) {
- return {
- ...item,
- subtotal_amount: emptyAmountDTO,
- discount_amount: emptyAmountDTO,
- taxable_amount: emptyAmountDTO,
- taxes_amount: emptyAmountDTO,
- total_amount: emptyAmountDTO,
- };
- }
-
- // 1) Subtotal = qty × unit
- const subtotal = unit ? multiply(unit, qty) : zero;
-
- // 2) Descuento = subtotal × (discount_percentage / 100)
- const pctDTO = item.discount_percentage ?? ({ value: "", scale: "" } as PercentageDTO);
- const pct = pctToNumber(pctDTO); // 0 si vacío
- const discountAmount = pct !== 0 ? moneyPct(subtotal, Math.abs(pct)) : zero;
-
- // 3) Base imponible = subtotal - descuento
- const baseAmount = sub(subtotal, discountAmount);
-
- // 4) Impuestos
- const taxesBreakdown =
- (item.tax_codes
- ?.map((code) => {
- const maybe = taxCatalog.findByCode(code);
- if (maybe.isNone()) {
- console.warn(`[useCalculateItemAmounts] Tax code not found: "${code}"`);
- return null;
- }
- const tax = maybe.unwrap()!; // { name, value, scale }
- const p = pctToNumber({ value: tax.value, scale: tax.scale }); // ej. 21
- return moneyPct(baseAmount, p);
- })
- .filter(Boolean) as MoneyDTO[]) ?? [];
-
- const taxesTotal = taxesBreakdown.length
- ? taxesBreakdown.reduce((acc, m) => add(acc, m), zero)
- : zero;
-
- // 5) Total = base + impuestos
- const total = add(baseAmount, taxesTotal);
-
- return {
- ...item,
- subtotal_amount: subtotal,
- discount_amount: discountAmount,
- taxable_amount: baseAmount,
- taxes_amount: taxesTotal,
- total_amount: total,
- };
- },
- [
- qtyToNumber,
- pctToNumber,
- multiply,
- moneyPct,
- add,
- sub,
- isEmptyMoneyDTO,
- zeroOf,
- toNumber,
- keepNullWhenEmpty,
- taxCatalog,
- ]
- );
-}
diff --git a/modules/customer-invoices/src/web/hooks/use-customer-invoices.bak b/modules/customer-invoices/src/web/hooks/use-customer-invoices.bak
deleted file mode 100644
index f85879b9..00000000
--- a/modules/customer-invoices/src/web/hooks/use-customer-invoices.bak
+++ /dev/null
@@ -1,75 +0,0 @@
-import { useDataSource, useQueryKey } from "@erp/core/hooks";
-import { IListCustomerInvoicesResponseDTO } from "@erp/customerInvoices/common/dto";
-
-export type UseCustomerInvoicesListParams = Omit & {
- status?: string;
- enabled?: boolean;
- queryOptions?: Record;
-};
-
-export type UseCustomerInvoicesListResponse = UseListQueryResult<
- IListResponseDTO,
- unknown
->;
-
-export type UseCustomerInvoicesGetParamsType = {
- enabled?: boolean;
- queryOptions?: Record;
-};
-
-export type UseCustomerInvoicesReportParamsType = {
- enabled?: boolean;
- queryOptions?: Record;
-};
-
-export const useCustomerInvoices = () => {
- const actions = {
- /**
- * Hook para obtener la lista de facturas
- * @param params - Parámetros para la consulta de la lista de facturas
- * @returns - Respuesta de la consulta de la lista de facturas
- */
- useList: (params: UseCustomerInvoicesListParams): UseCustomerInvoicesListResponse => {
- const dataSource = useDataSource();
- const keys = useQueryKey();
-
- const {
- pagination,
- status = "draft",
- quickSearchTerm = undefined,
- enabled = true,
- queryOptions,
- } = params;
-
- return useList({
- queryKey: keys().data().resource("customerInvoices").action("list").params(params).get(),
- queryFn: () => {
- return dataSource.getList({
- resource: "customerInvoices",
- quickSearchTerm,
- filters:
- status !== "all"
- ? [
- {
- field: "status",
- operator: "eq",
- value: status,
- },
- ]
- : [
- {
- field: "status",
- operator: "ne",
- value: "archived",
- },
- ],
- pagination,
- });
- },
- enabled,
- queryOptions,
- });
- },
- };
- return actions;
-};
diff --git a/packages/rdx-ui/package.json b/packages/rdx-ui/package.json
index 46aa7069..6f9a7eed 100644
--- a/packages/rdx-ui/package.json
+++ b/packages/rdx-ui/package.json
@@ -13,7 +13,7 @@
"./components": "./src/components/index.tsx",
"./components/*": "./src/components/*.tsx",
"./locales/*": "./src/locales/*",
- "./hooks/*": ["./src/hooks/*.tsx", "./src/hooks/*.ts"]
+ "./hooks": ["./src/hooks/index.ts"]
},
"peerDependencies": {
"date-fns": "^4.1.0",
diff --git a/packages/rdx-ui/src/hooks/index.ts b/packages/rdx-ui/src/hooks/index.ts
new file mode 100644
index 00000000..36b99b01
--- /dev/null
+++ b/packages/rdx-ui/src/hooks/index.ts
@@ -0,0 +1 @@
+export * from "./use-row-selection.ts";
diff --git a/packages/rdx-ui/src/hooks/use-row-selection.ts b/packages/rdx-ui/src/hooks/use-row-selection.ts
new file mode 100644
index 00000000..c9517ecb
--- /dev/null
+++ b/packages/rdx-ui/src/hooks/use-row-selection.ts
@@ -0,0 +1,55 @@
+import * as React from "react";
+
+export type CheckedState = boolean | "indeterminate";
+
+/**
+ * Hook para manejar selección múltiple con estado "seleccionar todo".
+ */
+export function useRowSelection(totalRows: number) {
+ const [selectedRows, setSelection] = React.useState>(new Set());
+
+ // Deriva array de índices seleccionados
+ const selectedIndexes = React.useMemo(
+ () => [...selectedRows].sort((a, b) => a - b),
+ [selectedRows]
+ );
+
+ // Estado visual del checkbox maestro
+ const selectAllState: CheckedState = React.useMemo(() => {
+ if (totalRows === 0 || selectedRows.size === 0) return false;
+ if (selectedRows.size === totalRows) return true;
+ return "indeterminate";
+ }, [selectedRows, totalRows]);
+
+ // Seleccionar/deseleccionar una fila
+ const toggleRow = React.useCallback((index: number) => {
+ setSelection((prev) => {
+ const next = new Set(prev);
+ next.has(index) ? next.delete(index) : next.add(index);
+ return next;
+ });
+ }, []);
+
+ // Seleccionar todas o limpiar
+ const setSelectAll = React.useCallback(
+ (checked: CheckedState) => {
+ if (checked === false) {
+ setSelection(new Set());
+ } else {
+ setSelection(new Set(Array.from({ length: totalRows }, (_, i) => i)));
+ }
+ },
+ [totalRows]
+ );
+
+ const clearSelection = React.useCallback(() => setSelection(new Set()), []);
+
+ return {
+ selectedRows,
+ selectedIndexes,
+ selectAllState,
+ toggleRow,
+ setSelectAll,
+ clearSelection,
+ };
+}
diff --git a/packages/rdx-ui/src/index.ts b/packages/rdx-ui/src/index.ts
index 3adb68f3..3bdf4f2b 100644
--- a/packages/rdx-ui/src/index.ts
+++ b/packages/rdx-ui/src/index.ts
@@ -2,3 +2,4 @@ export const PACKAGE_NAME = "rdx-ui";
export * from "./components/index.tsx";
export * from "./helpers/index.ts";
+export * from "./hooks/index.ts";