This commit is contained in:
David Arranz 2025-09-23 13:48:19 +02:00
parent bec5a54756
commit 01ca0bd5dc
8 changed files with 94 additions and 47 deletions

View File

@ -134,8 +134,8 @@ export const CustomerInvoicesListGrid = () => {
resizable: true, resizable: true,
}, },
pagination: true, pagination: true,
paginationPageSize: 10, paginationPageSize: 15,
paginationPageSizeSelector: [10, 20, 30, 50], paginationPageSizeSelector: [10, 15, 20, 30, 50],
localeText: AG_GRID_LOCALE_ES, localeText: AG_GRID_LOCALE_ES,
}), }),
[autoSizeStrategy, colDefs] [autoSizeStrategy, colDefs]

View File

@ -4,18 +4,18 @@ export const CreateCustomerRequestSchema = z.object({
id: z.string().nonempty(), id: z.string().nonempty(),
reference: z.string().optional(), reference: z.string().optional(),
is_company: z.string().toLowerCase().default("false"), is_company: z.string().toLowerCase().default("true"),
name: z.string(), name: z.string(),
trade_name: z.string().optional(), trade_name: z.string().optional(),
tin: z.string().optional(), tin: z.string().optional(),
default_taxes: z.array(z.string()).default([]).optional(), default_taxes: z.array(z.string()).default([]),
street: z.string().optional(), street: z.string().optional(),
street2: z.string().optional(), street2: z.string().optional(),
city: z.string().optional(), city: z.string().optional(),
province: z.string().optional(), province: z.string().optional(),
postal_code: z.string().optional(), postal_code: z.string().optional(),
country: z.string().default("es").optional(), country: z.string().toLowerCase().default("es").optional(),
email_primary: z.string().optional(), email_primary: z.string().optional(),
email_secondary: z.string().optional(), email_secondary: z.string().optional(),

View File

@ -31,7 +31,11 @@ export const CustomersListGrid = () => {
isLoading: isLoadingCustomers, isLoading: isLoadingCustomers,
isError: isLoadError, isError: isLoadError,
error: loadError, error: loadError,
} = useCustomersQuery(); } = useCustomersQuery({
pagination: {
pageSize: 999,
},
});
// Column Definitions: Defines & controls grid columns. // Column Definitions: Defines & controls grid columns.
const [colDefs] = useState<ColDef[]>([ const [colDefs] = useState<ColDef[]>([
@ -113,8 +117,8 @@ export const CustomersListGrid = () => {
resizable: true, resizable: true,
}, },
pagination: true, pagination: true,
paginationPageSize: 10, paginationPageSize: 15,
paginationPageSizeSelector: [10, 20, 30, 50], paginationPageSizeSelector: [10, 15, 20, 30, 50],
localeText: AG_GRID_LOCALE_ES, localeText: AG_GRID_LOCALE_ES,
}), }),
[autoSizeStrategy, colDefs] [autoSizeStrategy, colDefs]

View File

@ -22,6 +22,7 @@ export const CustomerBasicInfoFields = () => {
const { control } = useFormContext<CustomerFormData>(); const { control } = useFormContext<CustomerFormData>();
const isCompany = useWatch({ const isCompany = useWatch({
control,
name: "is_company", name: "is_company",
defaultValue: "true", defaultValue: "true",
}); });
@ -53,7 +54,9 @@ export const CustomerBasicInfoFields = () => {
<FormLabel>{t("form_fields.customer_type.label")}</FormLabel> <FormLabel>{t("form_fields.customer_type.label")}</FormLabel>
<FormControl> <FormControl>
<RadioGroup <RadioGroup
onValueChange={field.onChange} onValueChange={(value: string) => {
field.onChange(value === "false" ? "false" : "true");
}}
defaultValue={field.value ? "true" : "false"} defaultValue={field.value ? "true" : "false"}
className='flex items-center gap-6' className='flex items-center gap-6'
> >

View File

@ -2,7 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { FieldErrors, FormProvider, useForm } from "react-hook-form"; import { FieldErrors, FormProvider, useForm } from "react-hook-form";
import { useEffect } from "react"; import { useEffect } from "react";
import { CustomerFormData, CustomerFormSchema } from "../../schemas"; import { CreateCustomerFormSchema, CustomerFormData } from "../../schemas";
import { FormDebug } from "../form-debug"; import { FormDebug } from "../form-debug";
import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields"; import { CustomerAdditionalConfigFields } from "./customer-additional-config-fields";
import { CustomerAddressFields } from "./customer-address-fields"; import { CustomerAddressFields } from "./customer-address-fields";
@ -27,7 +27,7 @@ export function CustomerEditForm({
onDirtyChange, onDirtyChange,
}: CustomerFormProps) { }: CustomerFormProps) {
const form = useForm<CustomerFormData>({ const form = useForm<CustomerFormData>({
resolver: zodResolver(CustomerFormSchema), resolver: zodResolver(CreateCustomerFormSchema),
defaultValues: initialValues, defaultValues: initialValues,
disabled, disabled,
}); });

View File

@ -1,6 +1,7 @@
import { useDataSource } from "@erp/core/hooks"; import { useDataSource } from "@erp/core/hooks";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { CreateCustomerRequestSchema, CustomerCreationResponseDTO } from "../../common";
import { CustomerFormData } from "../schemas"; import { CustomerFormData } from "../schemas";
import { CUSTOMERS_LIST_KEY } from "./use-update-customer-mutation"; import { CUSTOMERS_LIST_KEY } from "./use-update-customer-mutation";
@ -11,19 +12,35 @@ type CreateCustomerPayload = {
export function useCreateCustomerMutation() { export function useCreateCustomerMutation() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const dataSource = useDataSource(); const dataSource = useDataSource();
const schema = CreateCustomerRequestSchema;
return useMutation<CustomerFormData, Error, CreateCustomerPayload>({ return useMutation<CustomerCreationResponseDTO, Error, CreateCustomerPayload>({
mutationKey: ["customer:create"], mutationKey: ["customer:create"],
mutationFn: async (payload) => { mutationFn: async (payload) => {
const { data } = payload; const { data } = payload;
const customerId = UniqueID.generateNewID(); const customerId = UniqueID.generateNewID();
const created = await dataSource.createOne("customers", { const newCustomerData = {
...data, ...data,
id: customerId.toString(), id: customerId.toString(),
}); };
return created as CustomerFormData;
const result = schema.safeParse(newCustomerData);
if (!result.success) {
// Construye errores detallados
const validationErrors = result.error.issues.map((err) => ({
field: err.path.join("."),
message: err.message,
}));
console.debug(validationErrors);
throw new ValidationErrorCollection("Validation failed", validationErrors);
}
const created = await dataSource.createOne("customers", newCustomerData);
return created as CustomerCreationResponseDTO;
}, },
onSuccess: () => { onSuccess: () => {

View File

@ -17,35 +17,35 @@ export const CustomerCreate = () => {
// 2) Estado de creación (mutación) // 2) Estado de creación (mutación)
const { const {
mutateAsync, mutate,
isPending: isCreating, isPending: isCreating,
isError: isCreateError, isError: isCreateError,
error: createError, error: createError,
} = useCreateCustomerMutation(); } = useCreateCustomerMutation();
// 3) Submit con navegación condicionada por éxito // 3) Submit con navegación condicionada por éxito
const handleSubmit = async (formData: CustomerFormData) => { const handleSubmit = (formData: CustomerFormData) => {
console.log("formData => ", formData); mutate(
{ data: formData },
{
onSuccess(data) {
setIsDirty(false);
showSuccessToast(t("pages.create.successTitle"), t("pages.create.successMsg"));
/*const changedData: Record<string, string> = {}; // El timeout es para que a React le dé tiempo a procesar
// el cambio de estado de isDirty / setIsDirty.
Object.keys(dirtyFields).forEach((field) => { setTimeout(() => {
const value = String(currentValues[field as keyof CustomerFormData]); navigate("/customers/list", {
changedData[field] = value; state: { customerId: data.id, isNew: true },
});*/ replace: true,
});
try { }, 0);
const result = await mutateAsync({ data: formData }); },
console.log(result); onError(error) {
showErrorToast(t("pages.create.errorTitle"), error.message);
if (result) { },
showSuccessToast(t("pages.create.successTitle"), t("pages.create.successMsg"));
navigate("/customers/list");
} }
} catch (e) { );
showErrorToast(t("pages.create.errorTitle"), (e as Error).message);
} finally {
}
}; };
const handleError = (errors: FieldErrors<CustomerFormData>) => { const handleError = (errors: FieldErrors<CustomerFormData>) => {

View File

@ -1,20 +1,30 @@
import * as z from "zod/v4"; import * as z from "zod/v4";
export const CustomerFormSchema = z.object({ export const CreateCustomerFormSchema = z.object({
reference: z.string().optional(), reference: z.string().optional(),
is_company: z.string().optional(), is_company: z.enum(["true", "false"]),
name: z.string().nonempty(), name: z
.string({
error: "El nombre es obligatorio",
})
.min(1, "El nombre no puede estar vacío"),
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 default_taxes: z.array(z.string()).default([]),
street: z.string().optional(), street: z.string().optional(),
street2: z.string().optional(), street2: z.string().optional(),
city: z.string().optional(), city: z.string().optional(),
province: z.string().optional(), province: z.string().optional(),
postal_code: z.string().optional(), postal_code: z.string().optional(),
country: z.string().optional(), country: z
.string({
error: "El país es obligatorio",
})
.min(1, "El país no puede estar vacío")
.toLowerCase() // asegura minúsculas
.default("es"),
email_primary: z.string().optional(), email_primary: z.string().optional(),
email_secondary: z.string().optional(), email_secondary: z.string().optional(),
@ -28,20 +38,33 @@ export const CustomerFormSchema = z.object({
legal_record: z.string().optional(), legal_record: z.string().optional(),
language_code: z.string().nonempty(), language_code: z
currency_code: z.string().nonempty(), .string({
error: "El idioma es obligatorio",
})
.min(1, "Debe indicar un idioma")
.toUpperCase() // asegura mayúsculas
.default("es"),
currency_code: z
.string({
error: "La moneda es obligatoria",
})
.min(1, "La moneda no puede estar vacía")
.toUpperCase() // asegura mayúsculas
.default("EUR"),
}); });
export type CustomerFormData = z.infer<typeof CustomerFormSchema>; export type CustomerFormData = z.infer<typeof CreateCustomerFormSchema>;
export const defaultCustomerFormData: CustomerFormData = { export const defaultCustomerFormData: CustomerFormData = {
reference: "", reference: "",
is_company: "false", is_company: "true",
name: "", name: "",
trade_name: "", trade_name: "",
tin: "", tin: "",
default_taxes: [], default_taxes: ["iva_21"],
street: "", street: "",
street2: "", street2: "",