Facturas de cliente
This commit is contained in:
parent
04edf3df68
commit
0f4619dd06
@ -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 */
|
||||
|
||||
@ -12,4 +12,8 @@ export type TaxItemType = {
|
||||
|
||||
export type TaxCatalogType = TaxItemType[];
|
||||
|
||||
export type TaxLookupItems = TaxItemType[];
|
||||
export type TaxLookupItems = {
|
||||
label: string;
|
||||
value: string;
|
||||
group: string;
|
||||
}[];
|
||||
|
||||
@ -100,6 +100,7 @@ export class CustomerInvoiceModel extends Model<
|
||||
CustomerInvoiceItemModel,
|
||||
CustomerModel,
|
||||
CustomerInvoiceTaxModel,
|
||||
VerifactuRecordModel,
|
||||
} = database.models;
|
||||
|
||||
CustomerInvoiceModel.belongsTo(CustomerModel, {
|
||||
|
||||
@ -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<string | undefined, string>();
|
||||
|
||||
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 (
|
||||
<div className={cn("w-full", "max-w-md")}>
|
||||
<MultiSelect
|
||||
options={taxesList}
|
||||
onValueChange={handleOnChange}
|
||||
onValidateOption={handleValidateOption}
|
||||
options={catalogLookup}
|
||||
onValueChange={onChange}
|
||||
defaultValue={value}
|
||||
placeholder={t("components.customer_invoice_taxes_multi_select.placeholder")}
|
||||
variant='inverted'
|
||||
animation={0}
|
||||
maxCount={3}
|
||||
autoFilter={true}
|
||||
filterSelected={filterSelectedByGroup}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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 */}
|
||||
<TableCell>
|
||||
<TaxMultiSelectField
|
||||
name={`items.${rowIndex}.tax_codes`}
|
||||
control={form.control}
|
||||
lookupCatalog={taxCatalog.toOptionLookup()}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
{/*<Controller
|
||||
<Controller
|
||||
control={control}
|
||||
name={`items.${rowIndex}.tax_codes`}
|
||||
render={({ field }) => (
|
||||
<TaxMultiSelect
|
||||
catalog={TAXES}
|
||||
value={field.value ?? ["iva_21"]}
|
||||
<CustomerInvoiceTaxesMultiSelect
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
disabled={readOnly}
|
||||
buttonClassName='h-8 self-start translate-y-[-1px]'
|
||||
/>
|
||||
)}
|
||||
/>*/}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
{/* taxes2 */}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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<HTMLButtonElement, MultiSelectProps>(
|
||||
@ -146,16 +155,33 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
asChild = false,
|
||||
className,
|
||||
selectAllVisible = false,
|
||||
filterSelected,
|
||||
autoFilter = false,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue);
|
||||
const [selectedValues, setSelectedValues] = React.useState<string[]>(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<Record<string, MultiSelectOptionType[]>>((acc, item) => {
|
||||
if (!acc[item.group || ""]) acc[item.group || ""] = [];
|
||||
acc[item.group || ""].push(item);
|
||||
@ -174,7 +200,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
};
|
||||
|
||||
const toggleOption = (option: string) => {
|
||||
if (onValidateOption && !onValidateOption(option)) {
|
||||
if (onValidateOption && !onValidateOption(option, selectedValues)) {
|
||||
console.warn(`Option "${option}" is not valid.`);
|
||||
return;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user