This commit is contained in:
David Arranz 2025-09-21 21:46:51 +02:00
parent 580beaba4c
commit b3c14b061b
18 changed files with 128 additions and 77 deletions

View File

@ -1,6 +1,6 @@
{ {
"common": { "common": {
"required": "required" "required": ""
}, },
"components": { "components": {
"taxes_multi_select": { "taxes_multi_select": {

View File

@ -1,6 +1,6 @@
{ {
"common": { "common": {
"required": "requerido" "required": ""
}, },
"components": { "components": {
"taxes_multi_select": { "taxes_multi_select": {

View File

@ -47,9 +47,15 @@ export function TaxesMultiSelectField<TFormValues extends FieldValues>({
render={({ field }) => ( render={({ field }) => (
<FormItem className={cn("space-y-0", className)}> <FormItem className={cn("space-y-0", className)}>
{label && ( {label && (
<div className='flex justify-between items-center'> <div className='mb-1 flex justify-between gap-2'>
<FormLabel className='m-0'>{label}</FormLabel> <div className='flex items-center gap-2'>
{required && <span className='text-xs text-destructive'>{t("common.required")}</span>} <FormLabel htmlFor={name} className='m-0'>
{label}
</FormLabel>
{required && (
<span className='text-xs text-destructive'>{t("common.required")}</span>
)}
</div>
</div> </div>
)} )}
<FormControl> <FormControl>

View File

@ -39,7 +39,7 @@ export class CustomerFullPresenter extends Presenter<Customer, GetCustomerByIdRe
legal_record: toEmptyString(customer.legalRecord, (value) => value.toPrimitive()), legal_record: toEmptyString(customer.legalRecord, (value) => value.toPrimitive()),
default_taxes: customer.defaultTaxes.getAll().join(", "), default_taxes: customer.defaultTaxes.getAll().map((tax) => tax.toString()),
status: customer.isActive ? "active" : "inactive", status: customer.isActive ? "active" : "inactive",
language_code: customer.languageCode.toPrimitive(), language_code: customer.languageCode.toPrimitive(),

View File

@ -1,4 +1,11 @@
import { DomainError, ValidationErrorCollection, ValidationErrorDetail } from "@repo/rdx-ddd"; import {
DomainError,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@repo/rdx-ddd";
import { UpdateCustomerByIdRequestDTO } from "../../../../common";
import { CustomerPatchProps } from "../../../domain";
import { import {
City, City,
@ -34,7 +41,7 @@ import { Collection, Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-ut
* *
*/ */
export function mapDTOToUpdateCustomerPatchProps(dto: UpdateCustomerRequestDTO) { export function mapDTOToUpdateCustomerPatchProps(dto: UpdateCustomerByIdRequestDTO) {
try { try {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const customerPatchProps: CustomerPatchProps = {}; const customerPatchProps: CustomerPatchProps = {};
@ -83,18 +90,50 @@ export function mapDTOToUpdateCustomerPatchProps(dto: UpdateCustomerRequestDTO)
); );
}); });
toPatchField(dto.email).ifSet((email) => { toPatchField(dto.email_primary).ifSet((email_primary) => {
customerPatchProps.email = extractOrPushError( customerPatchProps.emailPrimary = extractOrPushError(
maybeFromNullableVO(email, (value) => EmailAddress.create(value)), maybeFromNullableVO(email_primary, (value) => EmailAddress.create(value)),
"email", "email_primary",
errors errors
); );
}); });
toPatchField(dto.phone).ifSet((phone) => { toPatchField(dto.email_secondary).ifSet((email_secondary) => {
customerPatchProps.phone = extractOrPushError( customerPatchProps.emailSecondary = extractOrPushError(
maybeFromNullableVO(phone, (value) => PhoneNumber.create(value)), maybeFromNullableVO(email_secondary, (value) => EmailAddress.create(value)),
"phone", "email_secondary",
errors
);
});
toPatchField(dto.mobile_primary).ifSet((mobile_primary) => {
customerPatchProps.mobilePrimary = extractOrPushError(
maybeFromNullableVO(mobile_primary, (value) => PhoneNumber.create(value)),
"mobile_primary",
errors
);
});
toPatchField(dto.mobile_secondary).ifSet((mobile_secondary) => {
customerPatchProps.mobilePrimary = extractOrPushError(
maybeFromNullableVO(mobile_secondary, (value) => PhoneNumber.create(value)),
"mobile_secondary",
errors
);
});
toPatchField(dto.phone_primary).ifSet((phone_primary) => {
customerPatchProps.phonePrimary = extractOrPushError(
maybeFromNullableVO(phone_primary, (value) => PhoneNumber.create(value)),
"phone_primary",
errors
);
});
toPatchField(dto.phone_secondary).ifSet((phone_secondary) => {
customerPatchProps.phoneSecondary = extractOrPushError(
maybeFromNullableVO(phone_secondary, (value) => PhoneNumber.create(value)),
"phone_secondary",
errors errors
); );
}); });
@ -158,7 +197,7 @@ export function mapDTOToUpdateCustomerPatchProps(dto: UpdateCustomerRequestDTO)
return; return;
} }
defaultTaxes!.split(",").forEach((taxCode, index) => { defaultTaxes!.forEach((taxCode, index) => {
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors); const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors);
if (tax && customerPatchProps.defaultTaxes) { if (tax && customerPatchProps.defaultTaxes) {
customerPatchProps.defaultTaxes.add(tax); customerPatchProps.defaultTaxes.add(tax);
@ -173,7 +212,9 @@ export function mapDTOToUpdateCustomerPatchProps(dto: UpdateCustomerRequestDTO)
} }
if (errors.length > 0) { if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors)); return Result.fail(
new ValidationErrorCollection("Customer props mapping failed (update)", errors)
);
} }
return Result.ok(customerPatchProps); return Result.ok(customerPatchProps);
@ -183,7 +224,7 @@ export function mapDTOToUpdateCustomerPatchProps(dto: UpdateCustomerRequestDTO)
} }
function mapDTOToUpdatePostalAddressPatchProps( function mapDTOToUpdatePostalAddressPatchProps(
dto: UpdateCustomerRequestDTO, dto: UpdateCustomerByIdRequestDTO,
errors: ValidationErrorDetail[] errors: ValidationErrorDetail[]
): PostalAddressPatchProps | undefined { ): PostalAddressPatchProps | undefined {
const postalAddressPatchProps: PostalAddressPatchProps = {}; const postalAddressPatchProps: PostalAddressPatchProps = {};

View File

@ -197,7 +197,9 @@ export class CustomerDomainMapper
// Si hubo errores de mapeo, devolvemos colección de validación // Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) { if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection("Customer props mapping failed", errors)); return Result.fail(
new ValidationErrorCollection("Customer props mapping failed (CustomerMapper)", errors)
);
} }
const customerProps: CustomerProps = { const customerProps: CustomerProps = {

View File

@ -29,7 +29,7 @@ export const CreateCustomerRequestSchema = z.object({
legal_record: z.string().default(""), legal_record: z.string().default(""),
default_taxes: z.string().default(""), default_taxes: z.array(z.string()).default([]),
status: z.string().toLowerCase().default("active"), status: z.string().toLowerCase().default("active"),
language_code: z.string().toLowerCase().default("es"), language_code: z.string().toLowerCase().default("es"),
currency_code: z.string().toUpperCase().default("EUR"), currency_code: z.string().toUpperCase().default("EUR"),

View File

@ -11,6 +11,7 @@ export const UpdateCustomerByIdRequestSchema = z.object({
name: z.string().optional(), name: z.string().optional(),
trade_name: z.string().optional(), trade_name: z.string().optional(),
tin: z.string().optional(), tin: z.string().optional(),
default_taxes: z.array(z.string()).optional(), // completo (sustituye), o null => vaciar
street: z.string().optional(), street: z.string().optional(),
street2: z.string().optional(), street2: z.string().optional(),
@ -31,7 +32,6 @@ export const UpdateCustomerByIdRequestSchema = z.object({
legal_record: z.string().optional(), legal_record: z.string().optional(),
default_taxes: z.string().optional(), // completo (sustituye), o null => vaciar
language_code: z.string().optional(), language_code: z.string().optional(),
currency_code: z.string().optional(), currency_code: z.string().optional(),
}); });

View File

@ -25,7 +25,7 @@ export const CreateCustomerResponseSchema = z.object({
legal_record: z.string(), legal_record: z.string(),
default_taxes: z.string(), default_taxes: z.array(z.string()),
status: z.string(), status: z.string(),
language_code: z.string(), language_code: z.string(),
currency_code: z.string(), currency_code: z.string(),

View File

@ -30,7 +30,7 @@ export const GetCustomerByIdResponseSchema = z.object({
legal_record: z.string(), legal_record: z.string(),
default_taxes: z.string(), default_taxes: z.array(z.string()),
status: z.string(), status: z.string(),
language_code: z.string(), language_code: z.string(),
currency_code: z.string(), currency_code: z.string(),

View File

@ -25,7 +25,7 @@ export const ListCustomersResponseSchema = createListViewResponseSchema(
website: z.string(), website: z.string(),
//legal_record: z.string(), //legal_record: z.string(),
//default_taxes: z.string(), //default_taxes: z.array(z.string()),
language_code: z.string(), language_code: z.string(),
currency_code: z.string(), currency_code: z.string(),

View File

@ -30,7 +30,7 @@ export const UpdateCustomerByIdResponseSchema = z.object({
legal_record: z.string(), legal_record: z.string(),
default_taxes: z.string(), default_taxes: z.array(z.string()),
language_code: z.string(), language_code: z.string(),
currency_code: z.string(), currency_code: z.string(),

View File

@ -79,7 +79,7 @@ export const CustomersListGrid = () => {
size='icon' size='icon'
className='size-8' className='size-8'
onClick={() => { onClick={() => {
navigate(`${data.id}/edit`); navigate(`${data.id}/edit`, { relative: "path" });
}} }}
> >
<ChevronRightIcon /> <ChevronRightIcon />

View File

@ -1,5 +1,4 @@
import { TaxesMultiSelectField } from "@erp/core/components"; import { SelectField } from "@repo/rdx-ui/components";
import { SelectField, TextAreaField } from "@repo/rdx-ui/components";
import { import {
Card, Card,
CardContent, CardContent,
@ -23,41 +22,29 @@ export const CustomerAdditionalConfigFields = ({
<CardTitle>{t("form_groups.preferences.title")}</CardTitle> <CardTitle>{t("form_groups.preferences.title")}</CardTitle>
<CardDescription>{t("form_groups.preferences.description")}</CardDescription> <CardDescription>{t("form_groups.preferences.description")}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className='grid grid-cols-1 gap-y-8 gap-x-6 @xl:grid-cols-2'> <CardContent>
<TaxesMultiSelectField <div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-12 '>
control={control} <SelectField
name='default_taxes' className='lg:col-span-2'
required control={control}
label={t("form_fields.default_taxes.label")} name='language_code'
placeholder={t("form_fields.default_taxes.placeholder")} required
description={t("form_fields.default_taxes.description")} label={t("form_fields.language_code.label")}
/> placeholder={t("form_fields.language_code.placeholder")}
<SelectField description={t("form_fields.language_code.description")}
control={control} items={[...LANGUAGE_OPTIONS]}
name='language_code' />
required <SelectField
label={t("form_fields.language_code.label")} className='lg:col-span-2'
placeholder={t("form_fields.language_code.placeholder")} control={control}
description={t("form_fields.language_code.description")} name='currency_code'
items={[...LANGUAGE_OPTIONS]} required
/> label={t("form_fields.currency_code.label")}
<SelectField placeholder={t("form_fields.currency_code.placeholder")}
control={control} description={t("form_fields.currency_code.description")}
name='currency_code' items={[...CURRENCY_OPTIONS]}
required />
label={t("form_fields.currency_code.label")} </div>
placeholder={t("form_fields.currency_code.placeholder")}
description={t("form_fields.currency_code.description")}
items={[...CURRENCY_OPTIONS]}
/>
<TextAreaField
control={control}
name='legal_record'
required
label={t("form_fields.legal_record.label")}
placeholder={t("form_fields.legal_record.placeholder")}
description={t("form_fields.legal_record.description")}
/>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@ -55,7 +55,7 @@ export const CustomerAddressFields = ({ control }: { control: Control<CustomerUp
description={t("form_fields.postal_code.description")} description={t("form_fields.postal_code.description")}
/> />
</div> </div>
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-12 '> <div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-0 '>
<TextField <TextField
className='lg:col-span-2' className='lg:col-span-2'
control={control} control={control}

View File

@ -1,5 +1,5 @@
import { TaxesMultiSelectField } from "@erp/core/components"; import { TaxesMultiSelectField } from "@erp/core/components";
import { TextField } from "@repo/rdx-ui/components"; import { TextAreaField, TextField } from "@repo/rdx-ui/components";
import { import {
Card, Card,
CardContent, CardContent,
@ -27,8 +27,8 @@ export const CustomerBasicInfoFields = ({ control }: { control: Control<Customer
<CardTitle>Identificación</CardTitle> <CardTitle>Identificación</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className='grid grid-cols-1 gap-6 md:grid-cols-4'> <div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-12 '>
<div className='sm:col-span-full'> <div className='lg:col-span-full'>
<FormField <FormField
control={control} control={control}
name='is_company' name='is_company'
@ -64,8 +64,9 @@ export const CustomerBasicInfoFields = ({ control }: { control: Control<Customer
)} )}
/> />
</div> </div>
</div>
<div className='sm:col-span-2'> <div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-12 '>
<div className='lg:col-span-2'>
<TextField <TextField
control={control} control={control}
name='name' name='name'
@ -76,7 +77,7 @@ export const CustomerBasicInfoFields = ({ control }: { control: Control<Customer
/> />
</div> </div>
<div className='sm:col-span-2'> <div className='lg:col-span-2'>
<TextField <TextField
control={control} control={control}
name='trade_name' name='trade_name'
@ -86,7 +87,7 @@ export const CustomerBasicInfoFields = ({ control }: { control: Control<Customer
/> />
</div> </div>
<div className='sm:col-span-2'> <div className='lg:col-span-2'>
<TaxesMultiSelectField <TaxesMultiSelectField
control={control} control={control}
name='default_taxes' name='default_taxes'
@ -96,7 +97,7 @@ export const CustomerBasicInfoFields = ({ control }: { control: Control<Customer
description={t("form_fields.default_taxes.description")} description={t("form_fields.default_taxes.description")}
/> />
</div> </div>
<div className='col-auto'> <div className='lg:col-span-2'>
<TextField <TextField
control={control} control={control}
name='reference' name='reference'
@ -105,6 +106,14 @@ export const CustomerBasicInfoFields = ({ control }: { control: Control<Customer
description={t("form_fields.reference.description")} description={t("form_fields.reference.description")}
/> />
</div> </div>
<TextAreaField
className='lg:col-span-full'
control={control}
name='legal_record'
label={t("form_fields.legal_record.label")}
placeholder={t("form_fields.legal_record.placeholder")}
description={t("form_fields.legal_record.description")}
/>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -65,7 +65,7 @@ export const CustomerEditForm = ({ defaultValues, onSubmit, isPending }: Custome
return ( return (
<Form {...form}> <Form {...form}>
<FormDebug form={form} /> <FormDebug form={form} />
<form onSubmit={form.handleSubmit(handleSubmit, handleError)}> <form id='customer-edit-form' onSubmit={form.handleSubmit(handleSubmit, handleError)}>
<div className='flex gap-6'> <div className='flex gap-6'>
<div className='w-full xl:w-2/3 space-y-12'> <div className='w-full xl:w-2/3 space-y-12'>
<CustomerBasicInfoFields control={form.control} /> <CustomerBasicInfoFields control={form.control} />

View File

@ -46,9 +46,15 @@ export function TextAreaField<TFormValues extends FieldValues>({
render={({ field }) => ( render={({ field }) => (
<FormItem className={cn("space-y-0", className)}> <FormItem className={cn("space-y-0", className)}>
{label && ( {label && (
<div className='flex justify-between items-center'> <div className='mb-1 flex justify-between gap-2'>
<FormLabel className='m-0'>{label}</FormLabel> <div className='flex items-center gap-2'>
{required && <span className='text-xs text-destructive'>{t("common.required")}</span>} <FormLabel htmlFor={name} className='m-0'>
{label}
</FormLabel>
{required && (
<span className='text-xs text-destructive'>{t("common.required")}</span>
)}
</div>
</div> </div>
)} )}
<FormControl> <FormControl>