Facturas de cliente

This commit is contained in:
David Arranz 2025-10-08 19:41:38 +02:00
parent a946aa129a
commit 4e757c86d0
17 changed files with 282 additions and 325 deletions

View File

@ -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 , .

View File

@ -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 = () => (
<div className="space-y-2">
@ -33,16 +26,16 @@ export const HoverCardTotalsSummary = ({
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">{t("components.hover_card_totals_summary.fields.subtotal_amount")}:</span>
<span className="font-mono">{formatCurrency(summary.subtotal)}</span>
<span className="font-mono">{formatCurrency(totals.subtotal)}</span>
</div>
{Number(data.discount_percentage?.value ?? 0) > 0 && (
{(totals.discountPercent ?? 0) > 0 && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
{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}%):
</span>
<span className="font-mono text-destructive">
-{formatCurrency(summary.discountAmount)}
-{formatCurrency(totals.discountAmount)}
</span>
</div>
)}
@ -50,20 +43,20 @@ export const HoverCardTotalsSummary = ({
<div className="flex justify-between text-sm border-t pt-2">
<span className="text-muted-foreground">{t("components.hover_card_totals_summary.fields.taxable_amount")}:</span>
<span className="font-mono font-medium">
{formatCurrency(summary.baseAmount)}
{formatCurrency(totals.taxableBase)}
</span>
</div>
{summary.taxesBreakdown.map((tax) => (
{/*totals.taxesBreakdown.map((tax) => (
<div key={tax.label} className="flex justify-between text-sm">
<span className="text-muted-foreground">{tax.label}:</span>
<span className="font-mono">{formatCurrency(tax.amount)}</span>
</div>
))}
))*/}
<div className="flex justify-between text-sm border-t pt-2 font-semibold">
<span>{t("components.hover_card_totals_summary.fields.total_amount")}:</span>
<span className="font-mono">{formatCurrency(summary.total)}</span>
<span className="font-mono">{formatCurrency(totals.total)}</span>
</div>
</div>
)

View File

@ -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 (
<nav className="flex items-center justify-between h-12 py-1 px-2 text-muted-foreground bg-muted border-b">
<div className="flex items-center gap-2">
@ -109,7 +109,7 @@ export const ItemsEditorToolbar = ({
</div>
<div className="flex items-center gap-2">
<p className='text-sm font-normal'>
{t("common.rows_selected", { count: selectedIdx.length })}
{t("common.rows_selected", { count: selectedIndexes.length })}
</p>
</div>
</nav>

View File

@ -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<Set<number>>(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 (
<div className="space-y-0">
{/* Toolbar selección múltiple */}
<ItemsEditorToolbar
readOnly={readOnly}
selectedIdx={selectedIdx}
selectedIndexes={selectedIndexes}
onAdd={() => 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();
}} />
<div className="bg-background">
<Table className="w-full border-collapse text-sm">
@ -96,17 +88,9 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
<div className='h-5'>
<Checkbox
aria-label={t("common.select_all")}
className='block h-5 w-5 leading-none align-middle'
checked={selectAllState}
disabled={readOnly}
checked={getSelectAllState(tableNav.fa.fields.length, selection.size)}
onCheckedChange={(next) => {
const shouldSelectAll = next !== false;
if (shouldSelectAll) {
setSelection(new Set(tableNav.fa.fields.map((_, i) => i)));
} else {
resetSelection();
}
}}
onCheckedChange={(checked) => setSelectAll(checked)}
/>
</div>
</TableHead>
@ -122,11 +106,19 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
</TableHeader>
<TableBody className='text-sm'>
{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 (
<TableRow key={`row-${f.id}`} data-row-index={rowIndex}>
{/* selección */}
@ -134,11 +126,11 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
<div className='h-5'>
<Checkbox
aria-label={t("common.select_row", { n: rowIndex + 1 })}
className='block h-5 w-5 leading-none align-middle'
checked={selection.has(rowIndex)}
onCheckedChange={() => toggleSel(rowIndex)}
checked={selectedRows.has(rowIndex)}
disabled={readOnly}
/> </div>
onCheckedChange={() => toggleRow(rowIndex)}
/>
</div>
</TableCell>
{/* # */}
@ -222,15 +214,13 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
{/* total (solo lectura) */}
<TableCell className='text-right tabular-nums pt-[6px] leading-5'>
<HoverCardTotalsSummary data={{ ...f }}>
<HoverCardTotalsSummary totals={totals}>
<AmountDTOInputField
control={control}
name={`items.${rowIndex}.total_amount`}
readOnly
inputId={`total-amount-${rowIndex}`}
// @ts-expect-error
readOnlyMode='textlike-input'
locale={"es"}
locale="es"
/>
</HoverCardTotalsSummary>
</TableCell>
@ -298,7 +288,7 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
<TableCell colSpan={9}>
<ItemsEditorToolbar
readOnly={readOnly}
selectedIdx={selectedIdx}
selectedIndexes={selectedIndexes}
onAdd={() => tableNav.addEmpty(true)}
/>
</TableCell>

View File

@ -1,48 +0,0 @@
import * as React from "react";
import { parseNum } from './types.d';
type NumericInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
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<string>(() => value ?? value === 0 ? String(value) : "");
React.useEffect(() => { if (document.activeElement !== ref.current) setRaw(value ?? value === 0 ? String(value) : ""); }, [value]);
const ref = React.useRef<HTMLInputElement>(null);
return (
<input
ref={ref}
data-slot='input'
inputMode="decimal"
pattern="[0-9]*[.,]?[0-9]*"
className="w-full bg-transparent focus:outline-none p-0 text-right tabular-nums font-medium"
aria-live="off"
readOnly={readOnly}
value={raw}
onChange={(e) => 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}
/>
);
}

View File

@ -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<CustomerInvoiceItemFormData>();
const { format } = useMoney();
const calculateItemAmounts = useCalculateItemAmounts();
const [lines, setLines] = useState<CustomerInvoiceItemFormData[]>(items);
useEffect(() => {

View File

@ -0,0 +1,2 @@
export * from "./use-calc-invoice-items-totals";
export * from "./use-calc-invoice-totals";

View File

@ -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<InvoiceItemTotals>(() => {
// 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]);
}

View File

@ -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<InvoiceTotals>(() => {
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]);
}

View File

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

View File

@ -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,
]
);
}

View File

@ -1,75 +0,0 @@
import { useDataSource, useQueryKey } from "@erp/core/hooks";
import { IListCustomerInvoicesResponseDTO } from "@erp/customerInvoices/common/dto";
export type UseCustomerInvoicesListParams = Omit<IGetListDataProviderParams, "filters" | "resource"> & {
status?: string;
enabled?: boolean;
queryOptions?: Record<string, unknown>;
};
export type UseCustomerInvoicesListResponse = UseListQueryResult<
IListResponseDTO<IListCustomerInvoicesResponseDTO>,
unknown
>;
export type UseCustomerInvoicesGetParamsType = {
enabled?: boolean;
queryOptions?: Record<string, unknown>;
};
export type UseCustomerInvoicesReportParamsType = {
enabled?: boolean;
queryOptions?: Record<string, unknown>;
};
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;
};

View File

@ -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",

View File

@ -0,0 +1 @@
export * from "./use-row-selection.ts";

View File

@ -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<Set<number>>(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,
};
}

View File

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