Facturas de cliente
This commit is contained in:
parent
50dd4103b2
commit
04e3b835a0
@ -1,2 +1 @@
|
|||||||
export * from "./form-debug.tsx";
|
export * from "./form-debug.tsx";
|
||||||
export * from "./taxes-multi-select-field.tsx";
|
|
||||||
|
|||||||
@ -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<TFormValues extends FieldValues> = {
|
|
||||||
control: Control<TFormValues>;
|
|
||||||
name: FieldPath<TFormValues>;
|
|
||||||
label?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
description?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
readOnly?: boolean;
|
|
||||||
className?: string;
|
|
||||||
variant?: "default" | "secondary" | "destructive" | "inverted"
|
|
||||||
};
|
|
||||||
|
|
||||||
export function TaxesMultiSelectField<TFormValues extends FieldValues>({
|
|
||||||
control,
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
placeholder,
|
|
||||||
description,
|
|
||||||
disabled = false,
|
|
||||||
required = false,
|
|
||||||
readOnly = false,
|
|
||||||
className,
|
|
||||||
variant = "inverted",
|
|
||||||
}: TaxesMultiSelectFieldProps<TFormValues>) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const isDisabled = disabled || readOnly;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormField
|
|
||||||
control={control}
|
|
||||||
name={name}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className={cn("space-y-0", className)}>
|
|
||||||
{label && (
|
|
||||||
<div className='mb-1 flex justify-between'>
|
|
||||||
<div className='flex items-center gap-2'>
|
|
||||||
<FormLabel htmlFor={name} className='m-0'>
|
|
||||||
{label}
|
|
||||||
</FormLabel>
|
|
||||||
{required && (
|
|
||||||
<span className='text-xs text-destructive'>{t("common.required")}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<FormControl>
|
|
||||||
<TaxesMultiSelect disabled={isDisabled} placeholder={placeholder} variant={variant} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormDescription
|
|
||||||
className={cn("text-xs text-muted-foreground", !description && "invisible")}
|
|
||||||
>
|
|
||||||
{description || "\u00A0"}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -3,4 +3,3 @@ import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
|
|||||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||||
|
|
||||||
export * from "./form";
|
export * from "./form";
|
||||||
export * from "./taxes-multi-select";
|
|
||||||
|
|||||||
@ -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<TaxOption> = [
|
|
||||||
{ 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<string, TaxGroup>(
|
|
||||||
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<string>();
|
|
||||||
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 (
|
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<MultiSelect
|
|
||||||
options={[...TaxesList]}
|
|
||||||
value={value}
|
|
||||||
onValueChange={handleOnValueChange}
|
|
||||||
placeholder={t("components.taxes_multi_select.placeholder")}
|
|
||||||
variant={msVariant}
|
|
||||||
animation={0}
|
|
||||||
maxCount={3}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -73,7 +73,7 @@ export const CustomerInvoiceTaxesMultiSelect = (props: CustomerInvoiceTaxesMulti
|
|||||||
onValueChange={onChange}
|
onValueChange={onChange}
|
||||||
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='secondary'
|
||||||
animation={0}
|
animation={0}
|
||||||
maxCount={3}
|
maxCount={3}
|
||||||
autoFilter={true}
|
autoFilter={true}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { TaxesMultiSelectField } from "@erp/core/components";
|
|
||||||
import { Badge, Button, Input, Label } from "@repo/shadcn-ui/components";
|
import { Badge, Button, Input, Label } from "@repo/shadcn-ui/components";
|
||||||
import { Trash2 } from "lucide-react";
|
import { Trash2 } from "lucide-react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { CustomerInvoiceFormData } from "../../../schemas";
|
import { CustomerInvoiceFormData } from "../../../schemas";
|
||||||
|
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
|
||||||
import { CustomItemViewProps } from "./types";
|
import { CustomItemViewProps } from "./types";
|
||||||
|
|
||||||
export interface BlocksViewProps extends CustomItemViewProps { }
|
export interface BlocksViewProps extends CustomItemViewProps { }
|
||||||
@ -89,7 +89,7 @@ export const BlocksView = ({ items, removeItem, updateItem }: BlocksViewProps) =
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='space-y-2 col-start-1'>
|
<div className='space-y-2 col-start-1'>
|
||||||
<TaxesMultiSelectField
|
<CustomerInvoiceTaxesMultiSelect
|
||||||
control={control}
|
control={control}
|
||||||
name={`items.${index}.tax_codes`}
|
name={`items.${index}.tax_codes`}
|
||||||
required
|
required
|
||||||
|
|||||||
@ -1,3 +1 @@
|
|||||||
export * from "./blocks-view";
|
|
||||||
export * from "./items-editor";
|
export * from "./items-editor";
|
||||||
export * from "./table-view";
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { SpainTaxCatalogProvider } from '@erp/core';
|
import { SpainTaxCatalogProvider } from '@erp/core';
|
||||||
import { TaxesMultiSelect } from '@erp/core/components';
|
|
||||||
import { Button, Checkbox, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tooltip, TooltipContent, TooltipTrigger } from "@repo/shadcn-ui/components";
|
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 { ArrowDown, ArrowUp, CopyIcon, Trash2 } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
@ -13,7 +12,6 @@ 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 { TAXES } from './types.d';
|
|
||||||
|
|
||||||
interface ItemsEditorProps {
|
interface ItemsEditorProps {
|
||||||
value?: CustomerInvoiceItemFormData[];
|
value?: CustomerInvoiceItemFormData[];
|
||||||
@ -209,23 +207,6 @@ export const ItemsEditor = ({ value = [], onChange, readOnly = false }: ItemsEdi
|
|||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{/* taxes2 */}
|
|
||||||
<TableCell>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`items.${rowIndex}.tax_codes`}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TaxesMultiSelect
|
|
||||||
catalog={TAXES}
|
|
||||||
value={field.value ?? ["iva_21"]}
|
|
||||||
onChange={field.onChange}
|
|
||||||
disabled={readOnly}
|
|
||||||
buttonClassName='h-8 self-start translate-y-[-1px]'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
|
|
||||||
{/* total (solo lectura) */}
|
{/* total (solo lectura) */}
|
||||||
<TableCell className='text-right tabular-nums pt-[6px] leading-5'>
|
<TableCell className='text-right tabular-nums pt-[6px] leading-5'>
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import {
|
|||||||
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Plus, TrashIcon } from "lucide-react";
|
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Plus, TrashIcon } from "lucide-react";
|
||||||
|
|
||||||
|
|
||||||
import { TaxesMultiSelectField } from '@erp/core/components';
|
|
||||||
import { useMoney } from '@erp/core/hooks';
|
import { useMoney } from '@erp/core/hooks';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
@ -209,12 +208,6 @@ export const TableView = ({ items, actions }: TableViewProps) => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<TaxesMultiSelectField
|
|
||||||
control={control}
|
|
||||||
name={`item.${i}.tax_codes`}
|
|
||||||
required
|
|
||||||
placeholder={t("form_fields.item.tax_codes.placeholder")}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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<HTMLButtonElement>,
|
|
||||||
VariantProps<typeof multiSelectVariants> {
|
|
||||||
name: string;
|
|
||||||
control: Control<any>;
|
|
||||||
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<Record<string, TaxItemType[]>>((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<HTMLInputElement>) => {
|
|
||||||
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 (
|
|
||||||
<Popover open={open} onOpenChange={setOpen} modal={modalPopover}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
onClick={handleTogglePopover}
|
|
||||||
className={cn(
|
|
||||||
"flex w-full -mt-0.5 px-1 py-0.5 rounded-md border min-h-8 h-auto items-center justify-between bg-background hover:bg-inherit [&_svg]:pointer-events-auto",
|
|
||||||
buttonClassName
|
|
||||||
)}
|
|
||||||
disabled={disabled}
|
|
||||||
aria-label="Select taxes"
|
|
||||||
>
|
|
||||||
<div className="flex gap-1 flex-wrap">
|
|
||||||
{selected.length > 0 ? (
|
|
||||||
<div className='flex justify-between items-center w-full'>
|
|
||||||
<div className='flex flex-wrap items-center'>
|
|
||||||
{selected.slice(0, maxCount).map((item) => {
|
|
||||||
const option = lookupCatalog.find((o) => o.code === item?.code);
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
key={item?.code}
|
|
||||||
className={cn(
|
|
||||||
isAnimating ? "animate-bounce" : "",
|
|
||||||
multiSelectVariants({ variant })
|
|
||||||
)}
|
|
||||||
style={{ animationDuration: `${animation}s` }}
|
|
||||||
>
|
|
||||||
{option?.name}
|
|
||||||
{/*<XCircle
|
|
||||||
className='ml-2 h-4 w-4 cursor-pointer'
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
toggleOption(value);
|
|
||||||
}}
|
|
||||||
/>*/}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{selected.length > maxCount && (
|
|
||||||
<Badge
|
|
||||||
className={cn(
|
|
||||||
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
|
|
||||||
isAnimating ? "animate-bounce" : "",
|
|
||||||
multiSelectVariants({ variant })
|
|
||||||
)}
|
|
||||||
style={{ animationDuration: `${animation}s` }}
|
|
||||||
>
|
|
||||||
{`+ ${selected.length - maxCount} more`}
|
|
||||||
<XCircleIcon
|
|
||||||
className='ml-2 h-4 w-4 cursor-pointer'
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
clearExtraOptions();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className='flex items-center justify-between'>
|
|
||||||
<Separator orientation='vertical' className='flex min-h-6 h-full' />
|
|
||||||
<ChevronDownIcon className='h-4 mx-2 cursor-pointer text-muted-foreground' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className='flex items-center justify-between w-full mx-auto'>
|
|
||||||
<span className='text-sm text-muted-foreground mx-3'>
|
|
||||||
{placeholder || t("components.multi_select.select_options")}
|
|
||||||
</span>
|
|
||||||
<ChevronDownIcon className='h-4 cursor-pointer text-muted-foreground mx-2' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
|
|
||||||
<PopoverContent className="p-0 w-64" align="start">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder={t("common.search")} onKeyDown={handleInputKeyDown} />
|
|
||||||
|
|
||||||
<CommandList>
|
|
||||||
{Object.entries(grouped).map(([group, items], idx, arr) => (
|
|
||||||
<React.Fragment key={group}>
|
|
||||||
<CommandGroup key={`group-${group || "ungrouped"}`} heading={group}>
|
|
||||||
{items.map((t) => {
|
|
||||||
const active = value.includes(t.code);
|
|
||||||
return (
|
|
||||||
<CommandItem
|
|
||||||
key={t.code}
|
|
||||||
onSelect={() => toggleTax(t.code)}
|
|
||||||
aria-selected={active}
|
|
||||||
className='cursor-pointer'
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
|
||||||
active
|
|
||||||
? "bg-primary text-primary-foreground"
|
|
||||||
: "opacity-50 [&_svg]:invisible"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className={cn("h-4 w-4", active ? "text-primary-foreground" : "")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span>{t.name}</span>
|
|
||||||
</CommandItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CommandGroup>
|
|
||||||
{idx < arr.length - 1 && <Separator className="my-1" />}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</CommandList>
|
|
||||||
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
{animation > 0 && value.length > 0 && (
|
|
||||||
<WandSparklesIcon
|
|
||||||
className={cn(
|
|
||||||
"cursor-pointer my-2 text-foreground bg-background w-3 h-3",
|
|
||||||
isAnimating ? "" : "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
onClick={() => setIsAnimating(!isAnimating)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import { TaxesMultiSelectField } from "@erp/core/components";
|
|
||||||
import {
|
import {
|
||||||
Description,
|
Description,
|
||||||
Field,
|
Field,
|
||||||
@ -17,7 +16,8 @@ import {
|
|||||||
RadioGroup,
|
RadioGroup,
|
||||||
RadioGroupItem,
|
RadioGroupItem,
|
||||||
} from "@repo/shadcn-ui/components";
|
} 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 { useTranslation } from "../../i18n";
|
||||||
import { CustomerFormData } from "../../schemas";
|
import { CustomerFormData } from "../../schemas";
|
||||||
|
|
||||||
@ -107,13 +107,19 @@ export const CustomerBasicInfoFields = () => {
|
|||||||
description={t("form_fields.reference.description")}
|
description={t("form_fields.reference.description")}
|
||||||
/>
|
/>
|
||||||
<Field className='lg:col-span-2'>
|
<Field className='lg:col-span-2'>
|
||||||
<TaxesMultiSelectField
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name='default_taxes'
|
name='default_taxes'
|
||||||
required
|
render={({ field }) => (
|
||||||
label={t("form_fields.default_taxes.label")}
|
<CustomerInvoiceTaxesMultiSelect
|
||||||
placeholder={t("form_fields.default_taxes.placeholder")}
|
value={field.value}
|
||||||
description={t("form_fields.default_taxes.description")}
|
onChange={field.onChange}
|
||||||
|
required
|
||||||
|
label={t("form_fields.default_taxes.label")}
|
||||||
|
placeholder={t("form_fields.default_taxes.placeholder")}
|
||||||
|
description={t("form_fields.default_taxes.description")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<TextAreaField
|
<TextAreaField
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user