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 */
|
/** Devuelve un objeto indexado por código, compatible con TaxMultiSelectField */
|
||||||
toOptionLookup(): TaxLookupItems {
|
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 */
|
/** Devuelve la lista única de grupos disponibles */
|
||||||
|
|||||||
@ -12,4 +12,8 @@ export type TaxItemType = {
|
|||||||
|
|
||||||
export type TaxCatalogType = 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,
|
CustomerInvoiceItemModel,
|
||||||
CustomerModel,
|
CustomerModel,
|
||||||
CustomerInvoiceTaxModel,
|
CustomerInvoiceTaxModel,
|
||||||
|
VerifactuRecordModel,
|
||||||
} = database.models;
|
} = database.models;
|
||||||
|
|
||||||
CustomerInvoiceModel.belongsTo(CustomerModel, {
|
CustomerInvoiceModel.belongsTo(CustomerModel, {
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
import { SpainTaxCatalogProvider } from '@erp/core';
|
||||||
import { MultiSelect } from "@repo/rdx-ui/components";
|
import { MultiSelect } from "@repo/rdx-ui/components";
|
||||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from "../i18n";
|
import { useTranslation } from "../i18n";
|
||||||
|
|
||||||
const taxesList = [
|
const taxesList = [
|
||||||
@ -33,6 +35,8 @@ const taxesList = [
|
|||||||
{ label: "REC 0%", value: "rec_0", group: "Recargo de equivalencia" },
|
{ label: "REC 0%", value: "rec_0", group: "Recargo de equivalencia" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface CustomerInvoiceTaxesMultiSelect {
|
interface CustomerInvoiceTaxesMultiSelect {
|
||||||
value: string[];
|
value: string[];
|
||||||
onChange: (selectedValues: string[]) => void;
|
onChange: (selectedValues: string[]) => void;
|
||||||
@ -43,29 +47,37 @@ export const CustomerInvoiceTaxesMultiSelect = (props: CustomerInvoiceTaxesMulti
|
|||||||
const { value, onChange, ...otherProps } = props;
|
const { value, onChange, ...otherProps } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleOnChange = (selectedValues: string[]) => {
|
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
|
||||||
onChange(selectedValues);
|
const catalogLookup = useMemo(() => SpainTaxCatalogProvider().toOptionLookup(), []);
|
||||||
};
|
|
||||||
|
|
||||||
const handleValidateOption = (candidateValue: string) => {
|
/**
|
||||||
const exists = (value || []).some((item) => item.startsWith(candidateValue.substring(0, 3)));
|
* Filtra para mantener solo un elemento por grupo.
|
||||||
if (exists) {
|
* Si hay duplicados dentro del mismo grupo, se queda con el último.
|
||||||
alert(t("components.customer_invoice_taxes_multi_select.invalid_tax_selection"));
|
*/
|
||||||
}
|
const filterSelectedByGroup = useCallback((selectedValues: string[]) => {
|
||||||
return exists === false;
|
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 (
|
return (
|
||||||
<div className={cn("w-full", "max-w-md")}>
|
<div className={cn("w-full", "max-w-md")}>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
options={taxesList}
|
options={catalogLookup}
|
||||||
onValueChange={handleOnChange}
|
onValueChange={onChange}
|
||||||
onValidateOption={handleValidateOption}
|
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
placeholder={t("components.customer_invoice_taxes_multi_select.placeholder")}
|
placeholder={t("components.customer_invoice_taxes_multi_select.placeholder")}
|
||||||
variant='inverted'
|
variant='inverted'
|
||||||
animation={0}
|
animation={0}
|
||||||
maxCount={3}
|
maxCount={3}
|
||||||
|
autoFilter={true}
|
||||||
|
filterSelected={filterSelectedByGroup}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,12 +7,12 @@ import { Controller, useFormContext } from "react-hook-form";
|
|||||||
import { useCalculateItemAmounts, useItemsTableNavigation } from '../../../hooks';
|
import { useCalculateItemAmounts, useItemsTableNavigation } from '../../../hooks';
|
||||||
import { useTranslation } from '../../../i18n';
|
import { useTranslation } from '../../../i18n';
|
||||||
import { CustomerInvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
import { CustomerInvoiceItemFormData, defaultCustomerInvoiceItemFormData } from '../../../schemas';
|
||||||
|
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
|
||||||
import { AmountDTOInputField } from './amount-dto-input-field';
|
import { AmountDTOInputField } from './amount-dto-input-field';
|
||||||
import { HoverCardTotalsSummary } from './hover-card-total-summary';
|
import { HoverCardTotalsSummary } from './hover-card-total-summary';
|
||||||
import { ItemsEditorToolbar } from './items-editor-toolbar';
|
import { ItemsEditorToolbar } from './items-editor-toolbar';
|
||||||
import { PercentageDTOInputField } from './percentage-dto-input-field';
|
import { PercentageDTOInputField } from './percentage-dto-input-field';
|
||||||
import { QuantityDTOInputField } from './quantity-dto-input-field';
|
import { QuantityDTOInputField } from './quantity-dto-input-field';
|
||||||
import { TaxMultiSelectField } from './tax-multi-select-field';
|
|
||||||
import { TAXES } from './types.d';
|
import { TAXES } from './types.d';
|
||||||
|
|
||||||
interface ItemsEditorProps {
|
interface ItemsEditorProps {
|
||||||
@ -197,25 +197,16 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
|
|||||||
|
|
||||||
{/* taxes */}
|
{/* taxes */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<TaxMultiSelectField
|
<Controller
|
||||||
name={`items.${rowIndex}.tax_codes`}
|
|
||||||
control={form.control}
|
|
||||||
lookupCatalog={taxCatalog.toOptionLookup()}
|
|
||||||
disabled={readOnly}
|
|
||||||
/>
|
|
||||||
{/*<Controller
|
|
||||||
control={control}
|
control={control}
|
||||||
name={`items.${rowIndex}.tax_codes`}
|
name={`items.${rowIndex}.tax_codes`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<TaxMultiSelect
|
<CustomerInvoiceTaxesMultiSelect
|
||||||
catalog={TAXES}
|
value={field.value}
|
||||||
value={field.value ?? ["iva_21"]}
|
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
disabled={readOnly}
|
|
||||||
buttonClassName='h-8 self-start translate-y-[-1px]'
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>*/}
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{/* taxes2 */}
|
{/* taxes2 */}
|
||||||
|
|||||||
@ -16,17 +16,16 @@ export class VerifactuRecordModel extends Model<
|
|||||||
|
|
||||||
declare estado: string;
|
declare estado: string;
|
||||||
declare url: string;
|
declare url: string;
|
||||||
declare qr1: JSON;
|
declare qr: Blob;
|
||||||
declare qr2: Blob;
|
|
||||||
|
|
||||||
declare uuid: string;
|
declare uuid: string;
|
||||||
declare operacion: string;
|
declare operacion: string;
|
||||||
|
|
||||||
static associate(database: Sequelize) {
|
static associate(database: Sequelize) {
|
||||||
const { VerifactuRecordModel } = database.models;
|
const { CustomerInvoiceModel } = database.models;
|
||||||
|
|
||||||
VerifactuRecordModel.belongsTo(VerifactuRecordModel, {
|
VerifactuRecordModel.belongsTo(CustomerInvoiceModel, {
|
||||||
as: "verifactu-record",
|
as: "verifactu_records",
|
||||||
targetKey: "id",
|
targetKey: "id",
|
||||||
foreignKey: "invoice_id",
|
foreignKey: "invoice_id",
|
||||||
onDelete: "CASCADE",
|
onDelete: "CASCADE",
|
||||||
@ -59,12 +58,7 @@ export default (database: Sequelize) => {
|
|||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
qr1: {
|
qr: {
|
||||||
type: new DataTypes.JSON(),
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
qr2: {
|
|
||||||
type: new DataTypes.BLOB(),
|
type: new DataTypes.BLOB(),
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -82,7 +82,7 @@ export interface MultiSelectProps
|
|||||||
* Optional function to validate an option before selection.
|
* Optional function to validate an option before selection.
|
||||||
* Receives the option value and should return true if valid, false otherwise.
|
* 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. */
|
/** The default selected values when the component mounts. */
|
||||||
defaultValue?: string[];
|
defaultValue?: string[];
|
||||||
@ -129,6 +129,15 @@ export interface MultiSelectProps
|
|||||||
* Optional, defaults to false.
|
* Optional, defaults to false.
|
||||||
*/
|
*/
|
||||||
selectAllVisible?: boolean;
|
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>(
|
export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||||
@ -146,16 +155,33 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|||||||
asChild = false,
|
asChild = false,
|
||||||
className,
|
className,
|
||||||
selectAllVisible = false,
|
selectAllVisible = false,
|
||||||
|
filterSelected,
|
||||||
|
autoFilter = false,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue);
|
const [selectedValues, setSelectedValues] = React.useState<string[]>(defaultValue ?? []);
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
|
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
|
||||||
const [isAnimating, setIsAnimating] = React.useState(false);
|
const [isAnimating, setIsAnimating] = React.useState(false);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
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) => {
|
const grouped = options.reduce<Record<string, MultiSelectOptionType[]>>((acc, item) => {
|
||||||
if (!acc[item.group || ""]) acc[item.group || ""] = [];
|
if (!acc[item.group || ""]) acc[item.group || ""] = [];
|
||||||
acc[item.group || ""].push(item);
|
acc[item.group || ""].push(item);
|
||||||
@ -174,7 +200,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const toggleOption = (option: string) => {
|
const toggleOption = (option: string) => {
|
||||||
if (onValidateOption && !onValidateOption(option)) {
|
if (onValidateOption && !onValidateOption(option, selectedValues)) {
|
||||||
console.warn(`Option "${option}" is not valid.`);
|
console.warn(`Option "${option}" is not valid.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user