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> { public update(partialCustomer: CustomerPatchProps): Result<void, Error> {
const { address: partialAddress, ...rest } = partialCustomer; const { address: partialAddress, defaultTaxes: partialTaxes, ...rest } = partialCustomer;
Object.assign(this.props, rest); 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(); return Result.ok();
} }

View File

@ -17,11 +17,18 @@ export interface ICustomerItemTaxes {
toKey(): string; // Clave para representar un trío. toKey(): string; // Clave para representar un trío.
} }
export type CustomerTaxesPatchProps = Partial<CustomerTaxesProps>;
export class CustomerTaxes extends ValueObject<CustomerTaxesProps> implements ICustomerItemTaxes { export class CustomerTaxes extends ValueObject<CustomerTaxesProps> implements ICustomerItemTaxes {
static create(props: CustomerTaxesProps) { static create(props: CustomerTaxesProps) {
return Result.ok(new CustomerTaxes(props)); 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. * Reconstruye una instancia de CustomerTaxes a partir de una clave serializada.
* Este método es la operación inversa de toKey(). * 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 { FormFieldLabel, TextAreaField, TextField } from "@repo/rdx-ui/components";
import { import {
Field, Field,
FieldContent,
FieldDescription, FieldDescription,
FieldError, FieldError,
FieldGroup, FieldGroup,
FieldLabel, FieldLabel,
FieldLegend, FieldLegend,
FieldSet, FieldSet,
FormControl,
RadioGroup, RadioGroup,
RadioGroupItem, RadioGroupItem,
} from "@repo/shadcn-ui/components"; } from "@repo/shadcn-ui/components";
@ -19,15 +19,9 @@ import type { CustomerUpdateForm } from "../../entities";
import { CustomerTaxesMultiSelect } from "./customer-taxes-multi-select"; import { CustomerTaxesMultiSelect } from "./customer-taxes-multi-select";
interface CustomerBasicInfoFieldsProps extends React.ComponentProps<typeof FieldSet> { interface CustomerBasicInfoFieldsProps extends React.ComponentProps<typeof FieldSet> {}
focusRef?: React.RefObject<HTMLInputElement | null>;
}
export const CustomerBasicInfoFields = ({ export const CustomerBasicInfoFields = ({ className, ...props }: CustomerBasicInfoFieldsProps) => {
focusRef,
className,
...props
}: CustomerBasicInfoFieldsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { control, setFocus } = useFormContext<CustomerUpdateForm>(); const { control, setFocus } = useFormContext<CustomerUpdateForm>();
@ -50,49 +44,68 @@ export const CustomerBasicInfoFields = ({
required required
/> />
<Field className="lg:col-span-1 lg:col-start-1"> <Controller
<Controller control={control}
control={control} name="isCompany"
name="isCompany" render={({ field, fieldState }) => {
render={({ field, fieldState }) => ( console.log(field.value);
<Field className="gap-1" data-invalid={fieldState.invalid}> 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> <FormFieldLabel required>{t("form_fields.customer_type.label")}</FormFieldLabel>
<FormControl> <RadioGroup
<RadioGroup className="gap-3"
className="gap-3" disabled={field.disabled}
name={field.name} name={field.name}
onValueChange={(value) => field.onChange(value === "true")} onValueChange={(value) => {
value={String(field.value)} console.log("Pongo ", value);
> field.onChange(value === "true");
<div className="flex items-start gap-2"> }}
<RadioGroupItem id="customer-type-company" value="true" /> required
<label 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" className="cursor-pointer text-sm font-medium leading-none"
htmlFor="customer-type-company" htmlFor="customer-type-company"
> >
{t("form_fields.customer_type.company")} {t("form_fields.customer_type.company")}
</label> </FieldLabel>
</div> </FieldContent>
</Field>
<div className="flex items-start gap-2"> <Field data-invalid={fieldState.invalid} orientation="horizontal">
<RadioGroupItem id="customer-type-individual" value="false" /> <FieldContent>
<label <RadioGroupItem
aria-invalid={fieldState.invalid}
id="customer-type-individual"
value="false"
/>
<FieldLabel
className="cursor-pointer text-sm font-medium leading-none" className="cursor-pointer text-sm font-medium leading-none"
htmlFor="customer-type-individual" htmlFor="customer-type-individual"
> >
{t("form_fields.customer_type.individual")} {t("form_fields.customer_type.individual")}
</label> </FieldLabel>
</div> </FieldContent>
</RadioGroup> </Field>
</FormControl> </RadioGroup>
<FieldDescription>{t("form_fields.customer_type.description")}</FieldDescription> <FieldDescription>{t("form_fields.customer_type.description")}</FieldDescription>
<FieldError errors={[fieldState.error]} /> <FieldError errors={[fieldState.error]} />
</Field> </Field>
)} );
/> }}
</Field> />
<TextField <TextField
className="lg:col-span-1" className="lg:col-span-1"

View File

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

View File

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