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";