diff --git a/modules/core/src/common/catalogs/taxes/json-tax-catalog.provider.ts b/modules/core/src/common/catalogs/taxes/json-tax-catalog.provider.ts index 21f00834..1f7b9a0e 100644 --- a/modules/core/src/common/catalogs/taxes/json-tax-catalog.provider.ts +++ b/modules/core/src/common/catalogs/taxes/json-tax-catalog.provider.ts @@ -37,7 +37,11 @@ export class JsonTaxCatalogProvider implements TaxCatalogProvider { /** Devuelve un objeto indexado por código, compatible con TaxMultiSelectField */ toOptionLookup(): TaxLookupItems { - return Array.from(this.catalog.values()); + return this.getAll().map((item) => ({ + label: item.name, + value: item.code, + group: item.group, + })); } /** Devuelve la lista única de grupos disponibles */ diff --git a/modules/core/src/common/catalogs/taxes/tax-catalog-types.ts b/modules/core/src/common/catalogs/taxes/tax-catalog-types.ts index d4882787..927423f5 100644 --- a/modules/core/src/common/catalogs/taxes/tax-catalog-types.ts +++ b/modules/core/src/common/catalogs/taxes/tax-catalog-types.ts @@ -12,4 +12,8 @@ export type TaxItemType = { export type TaxCatalogType = TaxItemType[]; -export type TaxLookupItems = TaxItemType[]; +export type TaxLookupItems = { + label: string; + value: string; + group: string; +}[]; diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts index 709cb2d5..82e530f7 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts @@ -100,6 +100,7 @@ export class CustomerInvoiceModel extends Model< CustomerInvoiceItemModel, CustomerModel, CustomerInvoiceTaxModel, + VerifactuRecordModel, } = database.models; CustomerInvoiceModel.belongsTo(CustomerModel, { diff --git a/modules/customer-invoices/src/web/components/customer-invoice-taxes-multi-select.tsx b/modules/customer-invoices/src/web/components/customer-invoice-taxes-multi-select.tsx index b44c7908..b71d83ab 100644 --- a/modules/customer-invoices/src/web/components/customer-invoice-taxes-multi-select.tsx +++ b/modules/customer-invoices/src/web/components/customer-invoice-taxes-multi-select.tsx @@ -1,5 +1,7 @@ +import { SpainTaxCatalogProvider } from '@erp/core'; import { MultiSelect } from "@repo/rdx-ui/components"; import { cn } from "@repo/shadcn-ui/lib/utils"; +import { useCallback, useMemo } from 'react'; import { useTranslation } from "../i18n"; const taxesList = [ @@ -33,6 +35,8 @@ const taxesList = [ { label: "REC 0%", value: "rec_0", group: "Recargo de equivalencia" }, ]; + + interface CustomerInvoiceTaxesMultiSelect { value: string[]; onChange: (selectedValues: string[]) => void; @@ -43,29 +47,37 @@ export const CustomerInvoiceTaxesMultiSelect = (props: CustomerInvoiceTaxesMulti const { value, onChange, ...otherProps } = props; const { t } = useTranslation(); - const handleOnChange = (selectedValues: string[]) => { - onChange(selectedValues); - }; + const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []); + const catalogLookup = useMemo(() => SpainTaxCatalogProvider().toOptionLookup(), []); - const handleValidateOption = (candidateValue: string) => { - const exists = (value || []).some((item) => item.startsWith(candidateValue.substring(0, 3))); - if (exists) { - alert(t("components.customer_invoice_taxes_multi_select.invalid_tax_selection")); - } - return exists === false; - }; + /** + * Filtra para mantener solo un elemento por grupo. + * Si hay duplicados dentro del mismo grupo, se queda con el último. + */ + const filterSelectedByGroup = useCallback((selectedValues: string[]) => { + const groupMap = new Map(); + + selectedValues.forEach((code) => { + const item = taxCatalog.findByCode(code).getOrUndefined(); + const group = item?.group ?? "ungrouped"; + groupMap.set(group, code); // Sobrescribe el anterior del mismo grupo + }); + + return Array.from(groupMap.values()); + }, [taxCatalog]); 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 a1c5796d..5efa2817 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 @@ -7,12 +7,12 @@ import { Controller, useFormContext } from "react-hook-form"; import { useCalculateItemAmounts, useItemsTableNavigation } from '../../../hooks'; import { useTranslation } from '../../../i18n'; import { CustomerInvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas'; +import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select'; import { AmountDTOInputField } from './amount-dto-input-field'; import { HoverCardTotalsSummary } from './hover-card-total-summary'; import { ItemsEditorToolbar } from './items-editor-toolbar'; import { PercentageDTOInputField } from './percentage-dto-input-field'; import { QuantityDTOInputField } from './quantity-dto-input-field'; -import { TaxMultiSelectField } from './tax-multi-select-field'; import { TAXES } from './types.d'; interface ItemsEditorProps { @@ -197,25 +197,16 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi {/* taxes */} - - {/* ( - )} - />*/} + /> {/* taxes2 */} diff --git a/modules/verifactu/src/api/infrastructure/sequelize/models/verifactu-record.model.ts b/modules/verifactu/src/api/infrastructure/sequelize/models/verifactu-record.model.ts index 04d6b5d4..fe34ec5c 100644 --- a/modules/verifactu/src/api/infrastructure/sequelize/models/verifactu-record.model.ts +++ b/modules/verifactu/src/api/infrastructure/sequelize/models/verifactu-record.model.ts @@ -16,17 +16,16 @@ export class VerifactuRecordModel extends Model< declare estado: string; declare url: string; - declare qr1: JSON; - declare qr2: Blob; + declare qr: Blob; declare uuid: string; declare operacion: string; static associate(database: Sequelize) { - const { VerifactuRecordModel } = database.models; + const { CustomerInvoiceModel } = database.models; - VerifactuRecordModel.belongsTo(VerifactuRecordModel, { - as: "verifactu-record", + VerifactuRecordModel.belongsTo(CustomerInvoiceModel, { + as: "verifactu_records", targetKey: "id", foreignKey: "invoice_id", onDelete: "CASCADE", @@ -59,12 +58,7 @@ export default (database: Sequelize) => { allowNull: false, }, - qr1: { - type: new DataTypes.JSON(), - allowNull: false, - }, - - qr2: { + qr: { type: new DataTypes.BLOB(), allowNull: false, }, diff --git a/packages/rdx-ui/src/components/multi-select.tsx b/packages/rdx-ui/src/components/multi-select.tsx index e55c5e7c..0f86b5fe 100644 --- a/packages/rdx-ui/src/components/multi-select.tsx +++ b/packages/rdx-ui/src/components/multi-select.tsx @@ -82,7 +82,7 @@ export interface MultiSelectProps * Optional function to validate an option before selection. * Receives the option value and should return true if valid, false otherwise. */ - onValidateOption?: (value: string) => boolean; + onValidateOption?: (value: string, selectedValues: string[]) => boolean; /** The default selected values when the component mounts. */ defaultValue?: string[]; @@ -129,6 +129,15 @@ export interface MultiSelectProps * Optional, defaults to false. */ selectAllVisible?: boolean; + + + /** + * Filtra los items seleccionados + */ + filterSelected?: (selectedValues: string[]) => string[]; + + /** Si true, aplica el filtro automáticamente al cambiar los items seleccionados */ + autoFilter?: boolean; } export const MultiSelect = React.forwardRef( @@ -146,16 +155,33 @@ export const MultiSelect = React.forwardRef asChild = false, className, selectAllVisible = false, + filterSelected, + autoFilter = false, ...props }, ref ) => { - const [selectedValues, setSelectedValues] = React.useState(defaultValue); + const [selectedValues, setSelectedValues] = React.useState(defaultValue ?? []); const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); const [isAnimating, setIsAnimating] = React.useState(false); const { t } = useTranslation(); + const applySelectedFilter = React.useCallback(() => { + if (!filterSelected) return; + const filtered = filterSelected(selectedValues); + if (filtered.length !== selectedValues.length) { + setSelectedValues(filtered); + onValueChange(filtered); + } + }, [filterSelected, selectedValues, onValueChange]); + + // Filtro automático cuando cambia selectedValues + React.useEffect(() => { + if (autoFilter) applySelectedFilter(); + }, [autoFilter, selectedValues, applySelectedFilter]); + + const grouped = options.reduce>((acc, item) => { if (!acc[item.group || ""]) acc[item.group || ""] = []; acc[item.group || ""].push(item); @@ -174,7 +200,7 @@ export const MultiSelect = React.forwardRef }; const toggleOption = (option: string) => { - if (onValidateOption && !onValidateOption(option)) { + if (onValidateOption && !onValidateOption(option, selectedValues)) { console.warn(`Option "${option}" is not valid.`); return; }