Customers

This commit is contained in:
David Arranz 2026-04-04 01:39:03 +02:00
parent ce1c2c9b76
commit b35b89ee2b
5 changed files with 82 additions and 54 deletions

View File

@ -149,7 +149,7 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
}
public update(partialCustomer: CustomerPatchProps): Result<void, Error> {
const { address: partialAddress, ...rest } = partialCustomer;
const { address: partialAddress, defaultTaxes: partialTaxes, ...rest } = partialCustomer;
Object.assign(this.props, rest);
@ -161,6 +161,14 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
}
}
if (partialTaxes) {
const taxesResult = this.defaultTaxes.update(partialTaxes);
if (taxesResult.isFailure) {
return Result.fail(taxesResult.error);
}
}
return Result.ok();
}

View File

@ -17,11 +17,18 @@ export interface ICustomerItemTaxes {
toKey(): string; // Clave para representar un trío.
}
export type CustomerTaxesPatchProps = Partial<CustomerTaxesProps>;
export class CustomerTaxes extends ValueObject<CustomerTaxesProps> implements ICustomerItemTaxes {
static create(props: CustomerTaxesProps) {
return Result.ok(new CustomerTaxes(props));
}
public update(partial: CustomerTaxesPatchProps): Result<CustomerTaxes, Error> {
Object.assign(this.props, partial);
return Result.ok();
}
/**
* Reconstruye una instancia de CustomerTaxes a partir de una clave serializada.
* Este método es la operación inversa de toKey().

View File

@ -1,13 +1,13 @@
import { FormFieldLabel, TextAreaField, TextField } from "@repo/rdx-ui/components";
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FormControl,
RadioGroup,
RadioGroupItem,
} from "@repo/shadcn-ui/components";
@ -19,15 +19,9 @@ import type { CustomerUpdateForm } from "../../entities";
import { CustomerTaxesMultiSelect } from "./customer-taxes-multi-select";
interface CustomerBasicInfoFieldsProps extends React.ComponentProps<typeof FieldSet> {
focusRef?: React.RefObject<HTMLInputElement | null>;
}
interface CustomerBasicInfoFieldsProps extends React.ComponentProps<typeof FieldSet> {}
export const CustomerBasicInfoFields = ({
focusRef,
className,
...props
}: CustomerBasicInfoFieldsProps) => {
export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicInfoFieldsProps) => {
const { t } = useTranslation();
const { control, setFocus } = useFormContext<CustomerUpdateForm>();
@ -50,49 +44,68 @@ export const CustomerBasicInfoFields = ({
required
/>
<Field className="lg:col-span-1 lg:col-start-1">
<Controller
control={control}
name="isCompany"
render={({ field, fieldState }) => (
<Field className="gap-1" data-invalid={fieldState.invalid}>
<Controller
control={control}
name="isCompany"
render={({ field, fieldState }) => {
console.log(field.value);
return (
<Field
className="gap-1 lg:col-span-1 lg:col-start-1"
data-invalid={fieldState.invalid}
>
<FormFieldLabel required>{t("form_fields.customer_type.label")}</FormFieldLabel>
<FormControl>
<RadioGroup
className="gap-3"
name={field.name}
onValueChange={(value) => field.onChange(value === "true")}
value={String(field.value)}
>
<div className="flex items-start gap-2">
<RadioGroupItem id="customer-type-company" value="true" />
<label
<RadioGroup
className="gap-3"
disabled={field.disabled}
name={field.name}
onValueChange={(value) => {
console.log("Pongo ", value);
field.onChange(value === "true");
}}
required
value={field.value ? "true" : "false"}
>
<Field data-invalid={fieldState.invalid} orientation="horizontal">
<FieldContent>
<RadioGroupItem
aria-invalid={fieldState.invalid}
id="customer-type-company"
value="true"
/>
<FieldLabel
className="cursor-pointer text-sm font-medium leading-none"
htmlFor="customer-type-company"
>
{t("form_fields.customer_type.company")}
</label>
</div>
</FieldLabel>
</FieldContent>
</Field>
<div className="flex items-start gap-2">
<RadioGroupItem id="customer-type-individual" value="false" />
<label
<Field data-invalid={fieldState.invalid} orientation="horizontal">
<FieldContent>
<RadioGroupItem
aria-invalid={fieldState.invalid}
id="customer-type-individual"
value="false"
/>
<FieldLabel
className="cursor-pointer text-sm font-medium leading-none"
htmlFor="customer-type-individual"
>
{t("form_fields.customer_type.individual")}
</label>
</div>
</RadioGroup>
</FormControl>
</FieldLabel>
</FieldContent>
</Field>
</RadioGroup>
<FieldDescription>{t("form_fields.customer_type.description")}</FieldDescription>
<FieldError errors={[fieldState.error]} />
</Field>
)}
/>
</Field>
);
}}
/>
<TextField
className="lg:col-span-1"

View File

@ -9,14 +9,13 @@ type CustomerFormProps = {
formId: string;
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
className?: string;
focusRef?: React.RefObject<HTMLInputElement>;
};
export const CustomerEditForm = ({ formId, onSubmit, className, focusRef }: CustomerFormProps) => {
export const CustomerEditForm = ({ formId, onSubmit, className }: CustomerFormProps) => {
return (
<form id={formId} noValidate onSubmit={onSubmit}>
<section className={cn("space-y-12 p-6", className)}>
<CustomerBasicInfoFields focusRef={focusRef} />
<CustomerBasicInfoFields />
<CustomerAddressFields />
<CustomerContactFields />
<CustomerAdditionalConfigFields />

View File

@ -5,33 +5,34 @@ import { useCallback, useMemo } from "react";
import { useTranslation } from "../../../i18n";
interface CustomerTaxesMultiSelect {
interface CustomerTaxesMultiSelectProps {
value?: string[];
onChange: (selectedValues: string[]) => void;
className?: string;
inputId?: string;
[key: string]: any; // Allow other props to be passed
[key: string]: unknown;
}
export const CustomerTaxesMultiSelect = (props: CustomerTaxesMultiSelect) => {
const { value, onChange, className, inputId, ...otherProps } = props;
export const CustomerTaxesMultiSelect = ({
value,
onChange,
className,
inputId,
...otherProps
}: CustomerTaxesMultiSelectProps) => {
const { t } = useTranslation();
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
const catalogLookup = useMemo(() => SpainTaxCatalogProvider().toOptionLookup(), []);
const catalogLookup = useMemo(() => taxCatalog.toOptionLookup(), [taxCatalog]);
/**
* 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>();
const groupMap = new Map<string, string>();
for (const code of selectedValues) {
const item = taxCatalog.findByCode(code).getOrUndefined();
const group = item?.group ?? "ungrouped";
groupMap.set(group, code); // Sobrescribe el anterior del mismo grupo
groupMap.set(group, code);
}
return Array.from(groupMap.values());
@ -43,19 +44,19 @@ export const CustomerTaxesMultiSelect = (props: CustomerTaxesMultiSelect) => {
<div className={cn("w-full", "max-w-md")}>
<MultiSelect
animation={0}
autoFilter={true}
autoFilter
className={cn(
"flex w-full -mt-0.5 px-1 py-0.5 rounded-md border border-input min-h-8 h-auto items-center justify-between hover:bg-inherit [&_svg]:pointer-events-auto",
"hover:border-ring hover:ring-ring/50 hover:ring-2 font-medium bg-muted/50",
className
)}
defaultValue={value}
filterSelected={filterSelectedByGroup}
id={inputId}
maxCount={3}
onValueChange={onChange}
options={catalogLookup}
placeholder={t("components.customer_invoice_taxes_multi_select.placeholder")}
value={value ?? []}
variant="secondary"
{...otherProps}
/>