From 04e3b835a08cbf1448662e33a0acc8c1877c10bf Mon Sep 17 00:00:00 2001 From: david Date: Wed, 8 Oct 2025 18:50:48 +0200 Subject: [PATCH] Facturas de cliente --- modules/core/src/web/components/form/index.ts | 1 - .../form/taxes-multi-select-field.tsx | 77 ----- modules/core/src/web/components/index.ts | 1 - .../src/web/components/taxes-multi-select.tsx | 126 --------- .../customer-invoice-taxes-multi-select.tsx | 2 +- .../components/editor/items/blocks-view.tsx | 4 +- .../src/web/components/editor/items/index.ts | 2 - .../components/editor/items/items-editor.tsx | 19 -- .../components/editor/items/table-view.tsx | 7 - .../editor/items/tax-multi-select-field.tsx | 262 ------------------ .../editor/customer-basic-info-fields.tsx | 20 +- 11 files changed, 16 insertions(+), 505 deletions(-) delete mode 100644 modules/core/src/web/components/form/taxes-multi-select-field.tsx delete mode 100644 modules/core/src/web/components/taxes-multi-select.tsx delete mode 100644 modules/customer-invoices/src/web/components/editor/items/tax-multi-select-field.tsx diff --git a/modules/core/src/web/components/form/index.ts b/modules/core/src/web/components/form/index.ts index df2aa4f4..69a7258c 100644 --- a/modules/core/src/web/components/form/index.ts +++ b/modules/core/src/web/components/form/index.ts @@ -1,2 +1 @@ export * from "./form-debug.tsx"; -export * from "./taxes-multi-select-field.tsx"; diff --git a/modules/core/src/web/components/form/taxes-multi-select-field.tsx b/modules/core/src/web/components/form/taxes-multi-select-field.tsx deleted file mode 100644 index b24213f2..00000000 --- a/modules/core/src/web/components/form/taxes-multi-select-field.tsx +++ /dev/null @@ -1,77 +0,0 @@ -// DatePickerField.tsx - -import { - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@repo/shadcn-ui/components"; - -import { cn } from "@repo/shadcn-ui/lib/utils"; -import { Control, FieldPath, FieldValues } from "react-hook-form"; -import { useTranslation } from "../../i18n.ts"; -import { TaxesMultiSelect } from '../taxes-multi-select.tsx'; - -export type TaxesMultiSelectFieldProps = { - control: Control; - name: FieldPath; - label?: string; - placeholder?: string; - description?: string; - disabled?: boolean; - required?: boolean; - readOnly?: boolean; - className?: string; - variant?: "default" | "secondary" | "destructive" | "inverted" -}; - -export function TaxesMultiSelectField({ - control, - name, - label, - placeholder, - description, - disabled = false, - required = false, - readOnly = false, - className, - variant = "inverted", -}: TaxesMultiSelectFieldProps) { - const { t } = useTranslation(); - const isDisabled = disabled || readOnly; - - return ( - ( - - {label && ( -
-
- - {label} - - {required && ( - {t("common.required")} - )} -
-
- )} - - - - - - {description || "\u00A0"} - - -
- )} - /> - ); -} diff --git a/modules/core/src/web/components/index.ts b/modules/core/src/web/components/index.ts index 0d332a43..f25110c6 100644 --- a/modules/core/src/web/components/index.ts +++ b/modules/core/src/web/components/index.ts @@ -3,4 +3,3 @@ import { AllCommunityModule, ModuleRegistry } from "ag-grid-community"; ModuleRegistry.registerModules([AllCommunityModule]); export * from "./form"; -export * from "./taxes-multi-select"; diff --git a/modules/core/src/web/components/taxes-multi-select.tsx b/modules/core/src/web/components/taxes-multi-select.tsx deleted file mode 100644 index 4f3bd455..00000000 --- a/modules/core/src/web/components/taxes-multi-select.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { MultiSelect } from "@repo/rdx-ui/components"; -import { useTranslation } from "../i18n"; - -// Tipos -type TaxGroup = "IVA" | "Retención" | "Recargo de equivalencia"; - -interface TaxOption { - label: string; - value: string; - group: TaxGroup; -} - -export const TaxesList: ReadonlyArray = [ - { label: "IVA 21%", value: "iva_21", group: "IVA" }, - { label: "IVA 10%", value: "iva_10", group: "IVA" }, - { label: "IVA 7,5%", value: "iva_7_5", group: "IVA" }, - { label: "IVA 5%", value: "iva_5", group: "IVA" }, - { label: "IVA 4%", value: "iva_4", group: "IVA" }, - { label: "IVA 2%", value: "iva_2", group: "IVA" }, - { label: "IVA 0%", value: "iva_0", group: "IVA" }, - { label: "Exenta", value: "iva_exenta", group: "IVA" }, - { label: "No sujeto", value: "iva_no_sujeto", group: "IVA" }, - { label: "Iva Intracomunitario Bienes", value: "iva_intracomunitario_bienes", group: "IVA" }, - { label: "Iva Intracomunitario Servicio", value: "iva_intracomunitario_servicio", group: "IVA" }, - { label: "Exportación", value: "iva_exportacion", group: "IVA" }, - { label: "Inv. Suj. Pasivo", value: "iva_inversion_sujeto_pasivo", group: "IVA" }, - - { label: "Retención 35%", value: "retencion_35", group: "Retención" }, - { label: "Retención 19%", value: "retencion_19", group: "Retención" }, - { label: "Retención 15%", value: "retencion_15", group: "Retención" }, - { label: "Retención 7%", value: "retencion_7", group: "Retención" }, - { label: "Retención 2%", value: "retencion_2", group: "Retención" }, - - { label: "REC 5,2%", value: "rec_5_2", group: "Recargo de equivalencia" }, - { label: "REC 1,75%", value: "rec_1_75", group: "Recargo de equivalencia" }, - { label: "REC 1,4%", value: "rec_1_4", group: "Recargo de equivalencia" }, - { label: "REC 1%", value: "rec_1", group: "Recargo de equivalencia" }, - { label: "REC 0,62%", value: "rec_0_62", group: "Recargo de equivalencia" }, - { label: "REC 0,5%", value: "rec_0_5", group: "Recargo de equivalencia" }, - { label: "REC 0,26%", value: "rec_0_26", group: "Recargo de equivalencia" }, - { label: "REC 0%", value: "rec_0", group: "Recargo de equivalencia" }, -]; - -// Índices para lookup -const TAX_GROUP_BY_VALUE = new Map( - TaxesList.map((t) => [t.value, t.group]) -); - -// Util: deduplicar por grupo manteniendo el *último índice* de `arr` -function dedupeByTaxGroup(arr: readonly string[]): string[] { - const seen = new Set(); - const out: string[] = []; - for (let i = arr.length - 1; i >= 0; i--) { - const v = arr[i]; - const g = TAX_GROUP_BY_VALUE.get(v) ?? `__nogroup:${v}`; - if (!seen.has(g)) { - out.push(v); - seen.add(g); - } - } - out.reverse(); - return out; -} - -// Normaliza usando el *delta* para saber cuál fue el último clicado. -// Si se añadió uno, elimina los anteriores de su grupo. -// Si se quitaron, acepta `next` tal cual. Fallback: dedupe por índice. -function normalizeSelection(prev: readonly string[], next: readonly string[]): string[] { - const added = next.filter((v) => !prev.includes(v)); - const removed = prev.filter((v) => !next.includes(v)); - - if (added.length === 1 && removed.length <= 1) { - const clicked = added[0]; - const g = TAX_GROUP_BY_VALUE.get(clicked); - if (!g) return dedupeByTaxGroup(next); - return next.filter((v) => TAX_GROUP_BY_VALUE.get(v) !== g || v === clicked); - } - - // Multi-selección por teclado/ratón o reordenamientos del componente - return dedupeByTaxGroup(next); -} - - -interface TaxesMultiSelectProps { - name: string; - value: string[]; - variant?: "default" | "secondary" | "destructive" | "inverted" - onChange: (selectedValues: string[]) => void; - [key: string]: any; -} - -export const TaxesMultiSelect = (props: TaxesMultiSelectProps) => { - const { variant, value, onChange, ...otherProps } = props; - const { t } = useTranslation(); - - const msVariant = - variant as "default" | "secondary" | "destructive" | "inverted" | undefined; - - // IMPORTANTE: usar `value` (controlado). No usar `defaultValue`. - const handleOnValueChange = (nextValues: string[]) => { - const normalized = normalizeSelection(value, nextValues); - // Evitar renders innecesarios si no cambia - if ( - normalized.length === value.length && - normalized.every((v, i) => v === value[i]) - ) { - return; - } - onChange(normalized); - }; - - return ( -
- -
- ); -}; 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 b71d83ab..0c94822b 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 @@ -73,7 +73,7 @@ export const CustomerInvoiceTaxesMultiSelect = (props: CustomerInvoiceTaxesMulti onValueChange={onChange} defaultValue={value} placeholder={t("components.customer_invoice_taxes_multi_select.placeholder")} - variant='inverted' + variant='secondary' animation={0} maxCount={3} autoFilter={true} diff --git a/modules/customer-invoices/src/web/components/editor/items/blocks-view.tsx b/modules/customer-invoices/src/web/components/editor/items/blocks-view.tsx index 21eba4db..c92ee172 100644 --- a/modules/customer-invoices/src/web/components/editor/items/blocks-view.tsx +++ b/modules/customer-invoices/src/web/components/editor/items/blocks-view.tsx @@ -1,10 +1,10 @@ -import { TaxesMultiSelectField } from "@erp/core/components"; import { Badge, Button, Input, Label } from "@repo/shadcn-ui/components"; import { Trash2 } from "lucide-react"; import { useFormContext } from "react-hook-form"; import { useTranslation } from "../../../i18n"; import { CustomerInvoiceFormData } from "../../../schemas"; +import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select'; import { CustomItemViewProps } from "./types"; export interface BlocksViewProps extends CustomItemViewProps { } @@ -89,7 +89,7 @@ export const BlocksView = ({ items, removeItem, updateItem }: BlocksViewProps) =
- - {/* taxes2 */} - - ( - - )} - /> - - {/* total (solo lectura) */} 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 9da554a0..7db506ec 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 @@ -16,7 +16,6 @@ import { import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Plus, TrashIcon } from "lucide-react"; -import { TaxesMultiSelectField } from '@erp/core/components'; import { useMoney } from '@erp/core/hooks'; import { useEffect, useState } from 'react'; import { useFormContext } from 'react-hook-form'; @@ -209,12 +208,6 @@ export const TableView = ({ items, actions }: TableViewProps) => { - diff --git a/modules/customer-invoices/src/web/components/editor/items/tax-multi-select-field.tsx b/modules/customer-invoices/src/web/components/editor/items/tax-multi-select-field.tsx deleted file mode 100644 index be36435a..00000000 --- a/modules/customer-invoices/src/web/components/editor/items/tax-multi-select-field.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { TaxItemType, TaxLookupItems } from '@erp/core'; - -import { - Badge, - Button, - Command, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - Popover, - PopoverContent, - PopoverTrigger, - Separator -} from "@repo/shadcn-ui/components"; -import { cn } from "@repo/shadcn-ui/lib/utils"; -import { VariantProps, cva } from 'class-variance-authority'; -import { CheckIcon, ChevronDownIcon, WandSparklesIcon, XCircleIcon } from "lucide-react"; -import * as React from "react"; -import { Control, useController } from "react-hook-form"; -import { useTranslation } from 'react-i18next'; - - -/** - * Variants for the multi-select component to handle different styles. - * Uses class-variance-authority (cva) to define different styles based on "variant" prop. - */ -const multiSelectVariants = cva( - "m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300", - { - variants: { - variant: { - default: "border-foreground/10 text-foreground bg-card hover:bg-card/80", - secondary: - "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - inverted: "inverted", - }, - }, - defaultVariants: { - variant: "default", - }, - } -); - - -interface TaxMultiSelectFieldProps extends React.ButtonHTMLAttributes, - VariantProps { - name: string; - control: Control; - lookupCatalog: TaxLookupItems; - disabled?: boolean; - buttonClassName?: string; - - onValidateOption?: (value: string) => boolean; - placeholder?: string; - defaultValue?: string[]; - - maxCount?: number; // Maximum number of items to display - animation?: number; // Animation duration in seconds for the visual effects - modalPopover?: boolean; -} - -export const TaxMultiSelectField = ({ - name, - control, - lookupCatalog, - disabled, - buttonClassName, - variant, - - onValidateOption, - defaultValue = [], - placeholder, - maxCount = 3, - animation = 0, - modalPopover = false, -}: TaxMultiSelectFieldProps) => { - const { t } = useTranslation(); - const [isAnimating, setIsAnimating] = React.useState(true); - - const { field } = useController({ name, control }); - - const [open, setOpen] = React.useState(false); - - const value: string[] = field.value ?? []; - const selected = value.map((id) => lookupCatalog.find((item) => item.code === id)).filter(Boolean); - - // Agrupar catálogo por grupo - const grouped = React.useMemo(() => { - return Object.values(lookupCatalog).reduce>((acc, item) => { - if (!acc[item.group]) acc[item.group] = []; - acc[item.group].push(item); - return acc; - }, {}); - }, [lookupCatalog]); - - // Mantener un solo elemento por grupo - const toggleTax = (id: string) => { - const t = lookupCatalog.find((item) => item.code === id); - if (!t) return; - const active = value.includes(id); - let next: string[]; - if (active) { - next = value.filter((v) => v !== id); - } else { - next = [...value.filter((v) => lookupCatalog.find((item) => item.code === v)?.group !== t.group), id]; - } - field.onChange(next); - }; - - const handleInputKeyDown = (event: React.KeyboardEvent) => { - if (event.key === "Enter") { - setOpen(true); - } else if (event.key === "Backspace" && !event.currentTarget.value) { - const newSelectedValues = [...value]; - newSelectedValues.pop(); - field.onChange(newSelectedValues); - } - }; - - const handleTogglePopover = () => { - setOpen((prev) => !prev); - }; - - const clearExtraOptions = () => { - field.onChange(selected.slice(0, maxCount)); - }; - - const handleClear = () => { - field.onChange([]); - }; - - return ( - - - - - - - - - - - {Object.entries(grouped).map(([group, items], idx, arr) => ( - - - {items.map((t) => { - const active = value.includes(t.code); - return ( - toggleTax(t.code)} - aria-selected={active} - className='cursor-pointer' - > -
- -
- {t.name} -
- ); - })} -
- {idx < arr.length - 1 && } -
- ))} -
- -
-
- {animation > 0 && value.length > 0 && ( - setIsAnimating(!isAnimating)} - /> - )} -
- ); -} diff --git a/modules/customers/src/web/components/editor/customer-basic-info-fields.tsx b/modules/customers/src/web/components/editor/customer-basic-info-fields.tsx index 052c5bda..11e2bb1c 100644 --- a/modules/customers/src/web/components/editor/customer-basic-info-fields.tsx +++ b/modules/customers/src/web/components/editor/customer-basic-info-fields.tsx @@ -1,4 +1,3 @@ -import { TaxesMultiSelectField } from "@erp/core/components"; import { Description, Field, @@ -17,7 +16,8 @@ import { RadioGroup, RadioGroupItem, } from "@repo/shadcn-ui/components"; -import { useFormContext, useWatch } from "react-hook-form"; +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { CustomerInvoiceTaxesMultiSelect } from '../../../../../customer-invoices/src/web/components'; import { useTranslation } from "../../i18n"; import { CustomerFormData } from "../../schemas"; @@ -107,13 +107,19 @@ export const CustomerBasicInfoFields = () => { description={t("form_fields.reference.description")} /> - ( + + )} />