Repaso a customers
This commit is contained in:
parent
cabea02dcb
commit
e759da9916
@ -1,4 +0,0 @@
|
|||||||
//export * from "./use-create-customer-mutation";
|
|
||||||
//export * from "./use-customer-query";
|
|
||||||
//export * from "./use-customers-context";
|
|
||||||
//export * from "./use-update-customer-mutation";
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
|
||||||
import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
|
||||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { CreateCustomerRequestSchema } from "../../../common";
|
|
||||||
import { toValidationErrors } from "../../shared/hooks/to-validation-errors";
|
|
||||||
import type { Customer, CustomerFormData } from "../schemas";
|
|
||||||
|
|
||||||
import { CUSTOMERS_LIST_KEY, invalidateCustomerListCache } from "./use-customer-list-query";
|
|
||||||
import { setCustomerDetailCache } from "./use-customer-query";
|
|
||||||
|
|
||||||
export const CUSTOMER_CREATE_KEY = ["customers", "create"] as const;
|
|
||||||
|
|
||||||
type CreateCustomerPayload = {
|
|
||||||
data: CustomerFormData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useCreateCustomer() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const dataSource = useDataSource();
|
|
||||||
const schema = CreateCustomerRequestSchema;
|
|
||||||
|
|
||||||
return useMutation<Customer, DefaultError, CreateCustomerPayload>({
|
|
||||||
mutationKey: CUSTOMER_CREATE_KEY,
|
|
||||||
|
|
||||||
mutationFn: async ({ data }, context) => {
|
|
||||||
const id = UniqueID.generateNewID().toString();
|
|
||||||
const payload = { ...data, id };
|
|
||||||
|
|
||||||
const result = schema.safeParse(payload);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
|
||||||
}
|
|
||||||
|
|
||||||
const created = await dataSource.createOne("customers", payload);
|
|
||||||
return created as Customer;
|
|
||||||
},
|
|
||||||
|
|
||||||
onSuccess: (created: Customer, variables) => {
|
|
||||||
const { id: customerId } = created;
|
|
||||||
|
|
||||||
// Invalida el listado para refrescar desde servidor
|
|
||||||
invalidateCustomerListCache(queryClient);
|
|
||||||
|
|
||||||
// Sincroniza detalle
|
|
||||||
setCustomerDetailCache(queryClient, customerId, created);
|
|
||||||
},
|
|
||||||
|
|
||||||
onSettled: () => {
|
|
||||||
// Refresca todos los listados
|
|
||||||
invalidateCustomerListCache(queryClient);
|
|
||||||
},
|
|
||||||
|
|
||||||
onMutate: async ({ data }, context) => {
|
|
||||||
// Cancelar queries del listado para evitar overwrite
|
|
||||||
await queryClient.cancelQueries({ queryKey: [CUSTOMERS_LIST_KEY] });
|
|
||||||
|
|
||||||
const optimisticId = UniqueID.generateNewID().toString();
|
|
||||||
const optimisticCustomer: Customer = { ...data, id: optimisticId } as Customer;
|
|
||||||
|
|
||||||
// Snapshot previo
|
|
||||||
const previous = queryClient.getQueryData<Customer[]>([CUSTOMERS_LIST_KEY]);
|
|
||||||
|
|
||||||
// Optimista: prepend
|
|
||||||
queryClient.setQueryData<Customer[]>([CUSTOMERS_LIST_KEY], (old) => [
|
|
||||||
optimisticCustomer,
|
|
||||||
...(old ?? []),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { previous, optimisticId };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
|
||||||
import {
|
|
||||||
type DefaultError,
|
|
||||||
type QueryClient,
|
|
||||||
type QueryKey,
|
|
||||||
useQuery,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import type { Customer } from "../schemas";
|
|
||||||
|
|
||||||
export const CUSTOMER_DETAIL_SCOPE = "customers:detail" as const;
|
|
||||||
|
|
||||||
export const getCustomerQueryKey = (id: string) =>
|
|
||||||
[CUSTOMER_DETAIL_SCOPE, { id }] satisfies QueryKey;
|
|
||||||
|
|
||||||
type CustomerQueryOptions<TSelected> = {
|
|
||||||
enabled?: boolean;
|
|
||||||
staleTime?: number;
|
|
||||||
select?: (data: Customer) => TSelected;
|
|
||||||
placeholderData?: Customer;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useCustomerQuery<TSelected = Customer>(
|
|
||||||
customerId?: string,
|
|
||||||
options?: CustomerQueryOptions<TSelected>
|
|
||||||
) {
|
|
||||||
const dataSource = useDataSource();
|
|
||||||
const enabled = (options?.enabled ?? true) && Boolean(customerId);
|
|
||||||
|
|
||||||
return useQuery<Customer, DefaultError, TSelected>({
|
|
||||||
queryKey: getCustomerQueryKey(customerId ?? "unknown"),
|
|
||||||
queryFn: async (context) => {
|
|
||||||
const { signal } = context;
|
|
||||||
if (!customerId) {
|
|
||||||
if (!customerId) throw new Error("customerId is required");
|
|
||||||
}
|
|
||||||
return await dataSource.getOne<Customer>("customers", customerId, { signal });
|
|
||||||
},
|
|
||||||
enabled,
|
|
||||||
staleTime: options?.staleTime ?? 60_000, // 1 min por defecto
|
|
||||||
select: options?.select,
|
|
||||||
placeholderData: options?.placeholderData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function invalidateCustomerDetailCache(qc: QueryClient, id: string) {
|
|
||||||
return qc.invalidateQueries({
|
|
||||||
queryKey: getCustomerQueryKey(id ?? "unknown"),
|
|
||||||
exact: Boolean(id),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setCustomerDetailCache(qc: QueryClient, id: string, data: unknown) {
|
|
||||||
qc.setQueryData(getCustomerQueryKey(id), data);
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
import { CustomersContext, type CustomersContextType } from "../context";
|
|
||||||
|
|
||||||
export const useCustomersContext = (): CustomersContextType => {
|
|
||||||
const context = useContext(CustomersContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useCustomers must be used within a CustomersProvider");
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
|
||||||
import {
|
|
||||||
type DefaultError,
|
|
||||||
type QueryKey,
|
|
||||||
useMutation,
|
|
||||||
useQueryClient,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import type { CustomersPage } from "../schemas";
|
|
||||||
|
|
||||||
import { cancelCustomerListQueries, deleteCustomerIntoListCaches } from "./use-customer-list-query";
|
|
||||||
import { invalidateCustomerDetailCache } from "./use-customer-query";
|
|
||||||
|
|
||||||
export const CUSTOMER_DELETE_KEY = ["customers", "delete"] as const;
|
|
||||||
|
|
||||||
type DeleteCustomerPayload = {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useDeleteCustomer() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const dataSource = useDataSource();
|
|
||||||
|
|
||||||
return useMutation<{ id: string }, DefaultError, DeleteCustomerPayload>({
|
|
||||||
mutationKey: CUSTOMER_DELETE_KEY,
|
|
||||||
|
|
||||||
mutationFn: async ({ id: customerId }) => {
|
|
||||||
if (!customerId) {
|
|
||||||
throw new Error("customerId is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
await dataSource.deleteOne("customers", customerId);
|
|
||||||
return { id: customerId };
|
|
||||||
},
|
|
||||||
|
|
||||||
onMutate: async ({ id }) => {
|
|
||||||
await cancelCustomerListQueries(queryClient);
|
|
||||||
return deleteCustomerIntoListCaches(queryClient, id);
|
|
||||||
},
|
|
||||||
|
|
||||||
onError: (_e, _v, ctx) => {
|
|
||||||
if (!ctx) return;
|
|
||||||
const { snapshots } = ctx as ReturnType<typeof deleteCustomerIntoListCaches>;
|
|
||||||
for (const snap of snapshots as Array<{ key: QueryKey; page?: CustomersPage }>) {
|
|
||||||
if (snap.page) queryClient.setQueryData(snap.key, snap.page);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onSuccess: ({ id }) => {
|
|
||||||
invalidateCustomerDetailCache(queryClient, id);
|
|
||||||
},
|
|
||||||
|
|
||||||
onSettled: (data) => {
|
|
||||||
deleteCustomerIntoListCaches(queryClient, data?.id ?? "unknown");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
|
||||||
import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
|
||||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { UpdateCustomerByIdRequestSchema } from "../../../common";
|
|
||||||
import { toValidationErrors } from "../../shared/hooks/to-validation-errors";
|
|
||||||
import type { Customer, CustomerFormData } from "../schemas";
|
|
||||||
|
|
||||||
import {
|
|
||||||
invalidateCustomerListCache,
|
|
||||||
upsertCustomerIntoListCaches,
|
|
||||||
} from "./use-customer-list-query";
|
|
||||||
import { setCustomerDetailCache } from "./use-customer-query";
|
|
||||||
|
|
||||||
export const CUSTOMER_UPDATE_KEY = ["customers", "update"] as const;
|
|
||||||
|
|
||||||
type UpdateCustomerContext = {};
|
|
||||||
|
|
||||||
type UpdateCustomerPayload = {
|
|
||||||
id: string;
|
|
||||||
data: Partial<CustomerFormData>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useUpdateCustomer() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const dataSource = useDataSource();
|
|
||||||
const schema = UpdateCustomerByIdRequestSchema;
|
|
||||||
|
|
||||||
return useMutation<Customer, DefaultError, UpdateCustomerPayload, UpdateCustomerContext>({
|
|
||||||
mutationKey: CUSTOMER_UPDATE_KEY,
|
|
||||||
|
|
||||||
mutationFn: async (payload) => {
|
|
||||||
const { id: customerId, data } = payload;
|
|
||||||
if (!customerId) {
|
|
||||||
throw new Error("customerId is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = schema.safeParse(data);
|
|
||||||
if (!result.success) {
|
|
||||||
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = await dataSource.updateOne("customers", customerId, data);
|
|
||||||
return updated as Customer;
|
|
||||||
},
|
|
||||||
|
|
||||||
onSuccess: (updated: Customer, variables) => {
|
|
||||||
const { id: customerId } = updated;
|
|
||||||
|
|
||||||
// Invalida el listado para refrescar desde servidor
|
|
||||||
invalidateCustomerListCache(queryClient);
|
|
||||||
|
|
||||||
// Actualiza detalle
|
|
||||||
setCustomerDetailCache(queryClient, customerId, updated);
|
|
||||||
|
|
||||||
// Actualiza todas las páginas donde aparezca
|
|
||||||
upsertCustomerIntoListCaches(queryClient, { ...updated });
|
|
||||||
},
|
|
||||||
|
|
||||||
onSettled: () => {
|
|
||||||
// Refresca todos los listados
|
|
||||||
invalidateCustomerListCache(queryClient);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
import { PageHeader } from "@erp/core/components";
|
|
||||||
import { UnsavedChangesProvider, UpdateCommitButtonGroup } from "@erp/core/hooks";
|
|
||||||
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
|
||||||
import { CustomerEditForm, ErrorAlert } from "../../components";
|
|
||||||
|
|
||||||
import { useCustomerCreateController } from "./use-customer-create-controller";
|
|
||||||
|
|
||||||
export const CustomerCreatePage = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const {
|
|
||||||
form,
|
|
||||||
formId,
|
|
||||||
onSubmit,
|
|
||||||
resetForm,
|
|
||||||
|
|
||||||
isCreating,
|
|
||||||
isCreateError,
|
|
||||||
createError,
|
|
||||||
|
|
||||||
FormProvider,
|
|
||||||
} = useCustomerCreateController({
|
|
||||||
onCreated: (created) =>
|
|
||||||
navigate("/customers/list", {
|
|
||||||
state: { customerId: created.id, isNew: true },
|
|
||||||
replace: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UnsavedChangesProvider isDirty={form.formState.isDirty}>
|
|
||||||
<AppHeader>
|
|
||||||
<PageHeader
|
|
||||||
backIcon
|
|
||||||
description={t("pages.create.description")}
|
|
||||||
rightSlot={
|
|
||||||
<UpdateCommitButtonGroup
|
|
||||||
cancel={{
|
|
||||||
to: "/customers/list",
|
|
||||||
disabled: isCreating,
|
|
||||||
}}
|
|
||||||
disabled={isCreating}
|
|
||||||
isLoading={isCreating}
|
|
||||||
onReset={resetForm}
|
|
||||||
submit={{
|
|
||||||
formId: formId,
|
|
||||||
disabled: isCreating,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={t("pages.create.title")}
|
|
||||||
/>
|
|
||||||
</AppHeader>
|
|
||||||
<AppContent>
|
|
||||||
{/* Alerta de error de actualización (si ha fallado el último intento) */}
|
|
||||||
{isCreateError && (
|
|
||||||
<ErrorAlert
|
|
||||||
message={
|
|
||||||
(createError as Error)?.message ??
|
|
||||||
t("pages.create.errorMsg", "Revisa los datos e inténtalo de nuevo.")
|
|
||||||
}
|
|
||||||
title={t("pages.create.errorTitle", "No se pudo guardar los cambios")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormProvider {...form}>
|
|
||||||
<CustomerEditForm
|
|
||||||
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto mt-6" // para que el botón del header pueda hacer submit
|
|
||||||
formId={formId}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
/>
|
|
||||||
</FormProvider>
|
|
||||||
</AppContent>
|
|
||||||
</UnsavedChangesProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./customer-create-page";
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
import { useHookForm } from "@erp/core/hooks";
|
|
||||||
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
|
||||||
import { useId } from "react";
|
|
||||||
import { FormProvider } from "react-hook-form";
|
|
||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
|
||||||
import { useCreateCustomer } from "../../hooks";
|
|
||||||
import {
|
|
||||||
type Customer,
|
|
||||||
type CustomerFormData,
|
|
||||||
CustomerFormSchema,
|
|
||||||
defaultCustomerFormData,
|
|
||||||
} from "../../schemas";
|
|
||||||
|
|
||||||
export interface UseCustomerCreateControllerOptions {
|
|
||||||
onCreated?(created: Customer): void;
|
|
||||||
successToasts?: boolean; // mostrar o no toast automáticcamente
|
|
||||||
|
|
||||||
onError?(error: Error, data: CustomerFormData): void;
|
|
||||||
errorToasts?: boolean; // mostrar o no toast automáticcamente
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCustomerCreateController = (options?: UseCustomerCreateControllerOptions) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const formId = useId(); // id único por instancia
|
|
||||||
|
|
||||||
// 1) Estado de creación (mutación)
|
|
||||||
const {
|
|
||||||
mutateAsync,
|
|
||||||
isPending: isCreating,
|
|
||||||
isError: isCreateError,
|
|
||||||
error: createError,
|
|
||||||
} = useCreateCustomer();
|
|
||||||
|
|
||||||
// 2) Form hook
|
|
||||||
const form = useHookForm<CustomerFormData>({
|
|
||||||
resolverSchema: CustomerFormSchema,
|
|
||||||
initialValues: defaultCustomerFormData,
|
|
||||||
disabled: isCreating,
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Handlers */
|
|
||||||
|
|
||||||
const resetForm = () => form.reset(defaultCustomerFormData);
|
|
||||||
|
|
||||||
// Versión sincronizada
|
|
||||||
const submitHandler = form.handleSubmit(async (formData) => {
|
|
||||||
try {
|
|
||||||
// Enviamos cambios al servidor
|
|
||||||
const created = await mutateAsync({ data: formData });
|
|
||||||
|
|
||||||
// Ha ido bien -> actualizamos form con datos reales
|
|
||||||
// keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render.
|
|
||||||
form.reset(created, { keepDirty: false });
|
|
||||||
|
|
||||||
console.log("form.formState =>", form.formState);
|
|
||||||
|
|
||||||
if (options?.successToasts !== false) {
|
|
||||||
showSuccessToast(
|
|
||||||
t("pages.create.success.title", "Cliente creado"),
|
|
||||||
t("pages.create.success.message", "Se ha creado correctamente.")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
options?.onCreated?.(created);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
console.log("No se pudo crear el cliente.");
|
|
||||||
|
|
||||||
// 1) Enfoca el primer error
|
|
||||||
/*const firstKey = Object.keys(errors)[0] as keyof CustomerFormData | undefined;
|
|
||||||
if (firstKey) {
|
|
||||||
const el = document.querySelector<HTMLElement>(`[name="${String(firstKey)}"]`);
|
|
||||||
el?.focus();
|
|
||||||
}*/
|
|
||||||
|
|
||||||
const error = err as Error;
|
|
||||||
|
|
||||||
if (options?.errorToasts !== false) {
|
|
||||||
showErrorToast(t("pages.update.error.title"), error.message);
|
|
||||||
}
|
|
||||||
options?.onError?.(error, formData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Evento onSubmit ya preparado para el <form>
|
|
||||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
event.stopPropagation(); // <-- evita que el submit se propage por los padre en el árbol DOM
|
|
||||||
submitHandler(event);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
// form
|
|
||||||
form,
|
|
||||||
formId,
|
|
||||||
|
|
||||||
// handlers del form
|
|
||||||
onSubmit,
|
|
||||||
resetForm,
|
|
||||||
|
|
||||||
// mutation
|
|
||||||
isCreating,
|
|
||||||
isCreateError,
|
|
||||||
createError,
|
|
||||||
|
|
||||||
// Por comodidad
|
|
||||||
FormProvider,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import type { InvoiceItem } from "@/types/invoice";
|
|
||||||
|
|
||||||
export function calculateItemTotal(quantity: number, unitPrice: number, discount = 0): number {
|
|
||||||
const subtotal = quantity * unitPrice;
|
|
||||||
const discountAmount = (subtotal * discount) / 100;
|
|
||||||
return subtotal - discountAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateInvoiceTotals(items: InvoiceItem[], taxRate = 21) {
|
|
||||||
const subtotal = items.reduce((sum, item) => {
|
|
||||||
return (
|
|
||||||
sum + (item.quantity.amount * item.unit_price.amount) / Math.pow(10, item.unit_price.scale)
|
|
||||||
);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const totalDiscount = items.reduce((sum, item) => {
|
|
||||||
const itemSubtotal =
|
|
||||||
(item.quantity.amount * item.unit_price.amount) / Math.pow(10, item.unit_price.scale);
|
|
||||||
return sum + (itemSubtotal * item.discount.amount) / Math.pow(10, item.discount.scale) / 100;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const beforeTax = subtotal - totalDiscount;
|
|
||||||
const taxAmount = (beforeTax * taxRate) / 100;
|
|
||||||
const total = beforeTax + taxAmount;
|
|
||||||
|
|
||||||
return {
|
|
||||||
subtotal: Math.round(subtotal * 100),
|
|
||||||
totalDiscount: Math.round(totalDiscount * 100),
|
|
||||||
beforeTax: Math.round(beforeTax * 100),
|
|
||||||
taxAmount: Math.round(taxAmount * 100),
|
|
||||||
total: Math.round(total * 100),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatCurrency(amount: number, scale = 2, currency = "EUR"): string {
|
|
||||||
const value = amount / Math.pow(10, scale);
|
|
||||||
return new Intl.NumberFormat("es-ES", {
|
|
||||||
style: "currency",
|
|
||||||
currency: currency,
|
|
||||||
}).format(value);
|
|
||||||
}
|
|
||||||
@ -4,21 +4,20 @@ import { useId } from "react";
|
|||||||
import type { FieldErrors } from "react-hook-form";
|
import type { FieldErrors } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import type { Customer } from "../../shared";
|
import type { CreateCustomerParams, Customer } from "../../shared";
|
||||||
import { useCustomerCreateMutation } from "../../shared/hooks/use-customer-create-mutation";
|
import { useCustomerCreateMutation } from "../../shared/hooks/use-customer-create-mutation";
|
||||||
import {
|
import {
|
||||||
type CustomerCreateForm,
|
type CustomerCreateForm,
|
||||||
CustomerCreateFormSchema,
|
CustomerCreateFormSchema,
|
||||||
type CustomerCreatePayload,
|
|
||||||
defaultCustomerCreateForm,
|
defaultCustomerCreateForm,
|
||||||
} from "../entities";
|
} from "../entities";
|
||||||
import { buildCustomerCreatePayload } from "../utils";
|
import { buildCreateCustomerParams } from "../utils";
|
||||||
|
|
||||||
export interface UseCustomerCreateControllerOptions {
|
export interface UseCustomerCreateControllerOptions {
|
||||||
onCreated?(created: Customer): void;
|
onCreated?(created: Customer): void;
|
||||||
successToasts?: boolean; // mostrar o no toast automáticcamente
|
successToasts?: boolean; // mostrar o no toast automáticcamente
|
||||||
|
|
||||||
onError?(error: Error, payloadData: CustomerCreatePayload): void;
|
onError?(error: Error, inputPayload: CreateCustomerParams): void;
|
||||||
errorToasts?: boolean; // mostrar o no toast automáticcamente
|
errorToasts?: boolean; // mostrar o no toast automáticcamente
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,11 +48,11 @@ export const useCustomerCreateController = (options?: UseCustomerCreateControlle
|
|||||||
|
|
||||||
const submitHandler = form.handleSubmit(
|
const submitHandler = form.handleSubmit(
|
||||||
async (formData) => {
|
async (formData) => {
|
||||||
const payloadData: CustomerCreatePayload = buildCustomerCreatePayload(formData);
|
const inputPayload: CreateCustomerParams = buildCreateCustomerParams(formData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Enviamos cambios al servidor
|
// Enviamos cambios al servidor
|
||||||
const created = await mutateAsync(payloadData);
|
const created = await mutateAsync(inputPayload); // payload es CustomerCreatePayload
|
||||||
|
|
||||||
if (options?.successToasts !== false) {
|
if (options?.successToasts !== false) {
|
||||||
showSuccessToast(
|
showSuccessToast(
|
||||||
@ -70,7 +69,7 @@ export const useCustomerCreateController = (options?: UseCustomerCreateControlle
|
|||||||
showErrorToast(t("pages.create.error.title"), normalizedError.message);
|
showErrorToast(t("pages.create.error.title"), normalizedError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
options?.onError?.(normalizedError, payloadData);
|
options?.onError?.(normalizedError, inputPayload);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(errors: FieldErrors<CustomerCreateForm>) => {
|
(errors: FieldErrors<CustomerCreateForm>) => {
|
||||||
|
|||||||
@ -15,33 +15,33 @@ import { z } from "zod/v4";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const CustomerCreateFormSchema = z.object({
|
export const CustomerCreateFormSchema = z.object({
|
||||||
reference: z.string().optional().or(z.literal("")),
|
reference: z.string(),
|
||||||
isCompany: z.boolean(),
|
isCompany: z.boolean(),
|
||||||
name: z.string().min(1, "El nombre es obligatorio"),
|
name: z.string().min(1, "El nombre es obligatorio"),
|
||||||
tradeName: z.string().optional().or(z.literal("")),
|
tradeName: z.string(),
|
||||||
tin: z.string(),
|
tin: z.string(),
|
||||||
|
|
||||||
defaultTaxes: z.array(z.string()),
|
defaultTaxes: z.array(z.string()),
|
||||||
|
|
||||||
street: z.string().optional().or(z.literal("")),
|
street: z.string(),
|
||||||
street2: z.string().optional().or(z.literal("")),
|
street2: z.string(),
|
||||||
city: z.string().optional().or(z.literal("")),
|
city: z.string(),
|
||||||
province: z.string().optional().or(z.literal("")),
|
province: z.string(),
|
||||||
postalCode: z.string().optional().or(z.literal("")),
|
postalCode: z.string(),
|
||||||
country: z.string().min(1, "El país es obligatorio").optional().or(z.literal("")),
|
country: z.string().min(1, "El país es obligatorio"),
|
||||||
|
|
||||||
primaryEmail: z.email("Email inválido").optional().or(z.literal("")),
|
primaryEmail: z.email("Email inválido"),
|
||||||
secondaryEmail: z.email("Email inválido").optional().or(z.literal("")),
|
secondaryEmail: z.email("Email inválido"),
|
||||||
|
|
||||||
primaryPhone: z.string().optional().or(z.literal("")),
|
primaryPhone: z.string(),
|
||||||
secondaryPhone: z.string().optional().or(z.literal("")),
|
secondaryPhone: z.string(),
|
||||||
primaryMobile: z.string().optional().or(z.literal("")),
|
primaryMobile: z.string(),
|
||||||
secondaryMobile: z.string().optional().or(z.literal("")),
|
secondaryMobile: z.string(),
|
||||||
|
|
||||||
fax: z.string().optional().or(z.literal("")),
|
fax: z.string(),
|
||||||
website: z.url("URL inválida").optional().or(z.literal("")),
|
website: z.url("URL inválida"),
|
||||||
|
|
||||||
legalRecord: z.string().optional().or(z.literal("")),
|
legalRecord: z.string(),
|
||||||
|
|
||||||
languageCode: z.string().min(1, "El idioma es obligatorio"),
|
languageCode: z.string().min(1, "El idioma es obligatorio"),
|
||||||
currencyCode: z.string().min(1, "La moneda es obligatoria"),
|
currencyCode: z.string().min(1, "La moneda es obligatoria"),
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
import type { CustomerCreateForm } from "./customer-create-form.entity";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CustomerCreatePayload es un tipo que representa un objeto con las mismas
|
|
||||||
* propiedades que CustomerCreateForm, pero todas ellas son opcionales.
|
|
||||||
*
|
|
||||||
* Esto es útil para representar los datos que se van a enviar a la API para crear un cliente,
|
|
||||||
* ya que en una creación parcial (POST) no es necesario enviar todos los campos,
|
|
||||||
* sino solo aquellos que se quieren modificar.
|
|
||||||
*
|
|
||||||
* Reglas:
|
|
||||||
* - debe ser un Partial de CustomerCreateForm
|
|
||||||
* - no debe tener campos adicionales ni transformaciones
|
|
||||||
* - debe ser un shape orientado a la API, no a la UI ni al dominio
|
|
||||||
* - sin shape DTO, solo tipos simples y directos
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type CustomerCreatePayload = Partial<CustomerCreateForm> & {
|
|
||||||
// Aquí se añaden los campos que la API requiera para la creación de un cliente y que no estén en el formulario.
|
|
||||||
// Por ejemplo:
|
|
||||||
// - id: string;
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
export * from "./customer-create-form.entity";
|
export * from "./customer-create-form.entity";
|
||||||
export * from "./customer-create-form.schema";
|
export * from "./customer-create-form.schema";
|
||||||
export * from "./customer-create-form-default";
|
export * from "./customer-create-form-default";
|
||||||
export * from "./customer-create-payload.entity";
|
|
||||||
|
|||||||
1
modules/customers/src/web/create/index.ts
Normal file
1
modules/customers/src/web/create/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./ui";
|
||||||
@ -5,13 +5,17 @@ import { CustomerAddressFields } from "./customer-address-fields";
|
|||||||
import { CustomerBasicInfoFields } from "./customer-basic-info-fields";
|
import { CustomerBasicInfoFields } from "./customer-basic-info-fields";
|
||||||
import { CustomerContactFields } from "./customer-contact-fields";
|
import { CustomerContactFields } from "./customer-contact-fields";
|
||||||
|
|
||||||
type CustomerFormProps = {
|
type CustomerCreateEditorFormProps = {
|
||||||
formId: string;
|
formId: string;
|
||||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomerCreateForm = ({ formId, onSubmit, className }: CustomerFormProps) => {
|
export const CustomerCreateEditorForm = ({
|
||||||
|
formId,
|
||||||
|
onSubmit,
|
||||||
|
className,
|
||||||
|
}: CustomerCreateEditorFormProps) => {
|
||||||
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)}>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-create-editor-form";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./pages";
|
||||||
@ -5,8 +5,7 @@ import { FormProvider } from "react-hook-form";
|
|||||||
|
|
||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { useCustomerCreatePageController } from "../../controllers";
|
import { useCustomerCreatePageController } from "../../controllers";
|
||||||
|
import { CustomerCreateEditorForm } from "../editor";
|
||||||
import { CustomerCreateForm } from "./editor";
|
|
||||||
|
|
||||||
export const CustomerCreatePage = () => {
|
export const CustomerCreatePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -42,7 +41,7 @@ export const CustomerCreatePage = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<CustomerCreateForm
|
<CustomerCreateEditorForm
|
||||||
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto mt-6"
|
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto mt-6"
|
||||||
formId={formId}
|
formId={formId}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./customer-create-form";
|
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
|
|
||||||
|
import type { CreateCustomerParams } from "../../shared";
|
||||||
|
import type { CustomerCreateForm } from "../entities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construye un payload de creación de cliente a partir de los datos
|
||||||
|
* del formulario y los campos sucios.
|
||||||
|
*
|
||||||
|
* Reglas:
|
||||||
|
* - el payload debe ser un objeto con solo las propiedades que han cambiado (campos sucios).
|
||||||
|
* - no debe incluir campos que no han cambiado.
|
||||||
|
* - el shape del payload debe coincidir con el de CustomerCreatePayload,
|
||||||
|
* es decir, orientado a la API.
|
||||||
|
* - no debe tener transformaciones ni campos adicionales, solo los que vienen del
|
||||||
|
* formulario y están sucios.
|
||||||
|
*
|
||||||
|
* @param formData - Los datos del formulario de creación de cliente.
|
||||||
|
* @returns Un objeto que se puede enviar a la API para crear un cliente,
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const buildCreateCustomerParams = (formData: CustomerCreateForm): CreateCustomerParams => {
|
||||||
|
return {
|
||||||
|
// Generamos un ID único para el nuevo cliente.
|
||||||
|
// La API de creación de cliente requiere un ID,
|
||||||
|
id: UniqueID.generateNewID().toString(),
|
||||||
|
|
||||||
|
reference: formData.reference,
|
||||||
|
is_company: formData.isCompany ? "1" : "0",
|
||||||
|
name: formData.name,
|
||||||
|
trade_name: formData.tradeName,
|
||||||
|
tin: formData.tin,
|
||||||
|
default_taxes: formData.defaultTaxes,
|
||||||
|
|
||||||
|
street: formData.street,
|
||||||
|
street2: formData.street2,
|
||||||
|
city: formData.city,
|
||||||
|
province: formData.province,
|
||||||
|
postal_code: formData.postalCode,
|
||||||
|
country: formData.country,
|
||||||
|
|
||||||
|
email_primary: formData.primaryEmail,
|
||||||
|
email_secondary: formData.secondaryEmail,
|
||||||
|
phone_primary: formData.primaryPhone,
|
||||||
|
phone_secondary: formData.secondaryPhone,
|
||||||
|
mobile_primary: formData.primaryMobile,
|
||||||
|
mobile_secondary: formData.secondaryMobile,
|
||||||
|
|
||||||
|
fax: formData.fax,
|
||||||
|
website: formData.website,
|
||||||
|
|
||||||
|
legal_record: formData.legalRecord,
|
||||||
|
|
||||||
|
language_code: formData.languageCode,
|
||||||
|
currency_code: formData.currencyCode,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import { UniqueID } from "@repo/rdx-ddd";
|
|
||||||
|
|
||||||
import type { CustomerCreateForm, CustomerCreatePayload } from "../entities";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construye un payload de creación de cliente a partir de los datos
|
|
||||||
* del formulario y los campos sucios.
|
|
||||||
*
|
|
||||||
* Reglas:
|
|
||||||
* - el payload debe ser un objeto con solo las propiedades que han cambiado (campos sucios).
|
|
||||||
* - no debe incluir campos que no han cambiado.
|
|
||||||
* - el shape del payload debe coincidir con el de CustomerCreatePayload,
|
|
||||||
* es decir, orientado a la API.
|
|
||||||
* - no debe tener transformaciones ni campos adicionales, solo los que vienen del
|
|
||||||
* formulario y están sucios.
|
|
||||||
*
|
|
||||||
* @param formData: Los datos del formulario de creación de cliente.
|
|
||||||
* @returns Un objeto que se puede enviar a la API para crear un cliente,
|
|
||||||
* con todos los campos necesarios.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const buildCustomerCreatePayload = (formData: CustomerCreateForm): CustomerCreatePayload => {
|
|
||||||
return {
|
|
||||||
...formData,
|
|
||||||
|
|
||||||
// El backend exige que el cliente envíe un id en la creación.
|
|
||||||
id: UniqueID.generateNewID().toString(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1 +1 @@
|
|||||||
export * from "./build-customer-create-payload";
|
export * from "./build-customer-create-params";
|
||||||
|
|||||||
@ -9,7 +9,9 @@ const CustomerLayout = lazy(() =>
|
|||||||
const CustomersList = lazy(() => import("./list").then((m) => ({ default: m.ListCustomersPage })));
|
const CustomersList = lazy(() => import("./list").then((m) => ({ default: m.ListCustomersPage })));
|
||||||
const CustomerView = lazy(() => import("./view").then((m) => ({ default: m.CustomerViewPage })));
|
const CustomerView = lazy(() => import("./view").then((m) => ({ default: m.CustomerViewPage })));
|
||||||
|
|
||||||
const CustomerAdd = lazy(() => import("./create").then((m) => ({ default: m.CustomerCreatePage })));
|
const CustomerCreate = lazy(() =>
|
||||||
|
import("./create").then((m) => ({ default: m.CustomerCreatePage }))
|
||||||
|
);
|
||||||
const CustomerUpdate = lazy(() =>
|
const CustomerUpdate = lazy(() =>
|
||||||
import("./update").then((m) => ({ default: m.CustomerUpdatePage }))
|
import("./update").then((m) => ({ default: m.CustomerUpdatePage }))
|
||||||
);
|
);
|
||||||
@ -29,9 +31,10 @@ export const CustomerRoutes = (params: ModuleClientParams): RouteObject[] => {
|
|||||||
//{ path: "create", element: <CustomerAdd /> },
|
//{ path: "create", element: <CustomerAdd /> },
|
||||||
{ path: ":id", element: <CustomerView /> },
|
{ path: ":id", element: <CustomerView /> },
|
||||||
{ path: ":id/edit", element: <CustomerUpdate /> },
|
{ path: ":id/edit", element: <CustomerUpdate /> },
|
||||||
|
{ path: "create", element: <CustomerCreate /> },
|
||||||
|
|
||||||
//
|
//
|
||||||
/*{ path: "create", element: <CustomersList /> },
|
/*
|
||||||
{ path: ":id", element: <CustomersList /> },
|
{ path: ":id", element: <CustomersList /> },
|
||||||
{ path: ":id/edit", element: <CustomersList /> },
|
{ path: ":id/edit", element: <CustomersList /> },
|
||||||
{ path: ":id/delete", element: <CustomersList /> },
|
{ path: ":id/delete", element: <CustomersList /> },
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { CriteriaDTO } from "@erp/core";
|
|||||||
import { useDebounce } from "@repo/rdx-ui/components";
|
import { useDebounce } from "@repo/rdx-ui/components";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
import { useListCustomersQuery } from "../../shared";
|
import { useCustomerListQuery } from "../../shared";
|
||||||
|
|
||||||
export const useListCustomersController = () => {
|
export const useListCustomersController = () => {
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
@ -21,7 +21,7 @@ export const useListCustomersController = () => {
|
|||||||
};
|
};
|
||||||
}, [pageSize, pageIndex, debouncedQ]);
|
}, [pageSize, pageIndex, debouncedQ]);
|
||||||
|
|
||||||
const query = useListCustomersQuery({ criteria });
|
const query = useCustomerListQuery({ criteria });
|
||||||
|
|
||||||
const setSearchValue = (value: string) => {
|
const setSearchValue = (value: string) => {
|
||||||
const nextValue = value.trim().replace(/\s+/g, " ");
|
const nextValue = value.trim().replace(/\s+/g, " ");
|
||||||
|
|||||||
@ -1,6 +1,18 @@
|
|||||||
import type { Customer } from "../entities/customer.entity";
|
import type { Customer } from "../entities/customer.entity";
|
||||||
import type { CustomerListRow } from "../entities/customer-list-row.entity";
|
import type { CustomerListRow } from "../entities/customer-list-row.entity";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adaptador para transformar un objeto Customer a un objeto CustomerListRowPatch,
|
||||||
|
* que es una versión parcial de CustomerListRow.
|
||||||
|
* Utilizado por el gestor de caché para actualizar algunos campos del objeto CustomerListRow.
|
||||||
|
* Reglas de adaptación:
|
||||||
|
* - los campos se asignan directamente
|
||||||
|
* - los campos son opcionales excepto el id.
|
||||||
|
*
|
||||||
|
* @param customer - objeto Customer a adaptar.
|
||||||
|
* @returns {CustomerListRowPatch} Objeto adaptado a CustomerListRowPatch.
|
||||||
|
*/
|
||||||
|
|
||||||
export type CustomerListRowPatch = Pick<CustomerListRow, "id"> & Partial<CustomerListRow>;
|
export type CustomerListRowPatch = Pick<CustomerListRow, "id"> & Partial<CustomerListRow>;
|
||||||
|
|
||||||
export const CustomerToListRowPatchAdapter = {
|
export const CustomerToListRowPatchAdapter = {
|
||||||
|
|||||||
@ -1,11 +1,24 @@
|
|||||||
import type { CustomerCreationResponseDTO } from "@erp/customers/common";
|
import type { CustomerCreationResponseDTO } from "@erp/customers/common";
|
||||||
|
|
||||||
import type { CustomerGetOutput, CustomerUpdateOutput } from "../api";
|
import type { GetCustomerByIdResult, UpdateCustomerByIdResult } from "../api";
|
||||||
import type { Customer } from "../entities";
|
import type { Customer } from "../entities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adaptador para transformar los datos de la API de GetCustomerByIdResult, CustomerCreationResponseDTO o UpdateCustomerByIdResult
|
||||||
|
* a la entidad Customer utilizada en la aplicación.
|
||||||
|
* Reglas de adaptación:
|
||||||
|
* - id, company_id, reference se asignan directamente.
|
||||||
|
* - is_company se convierte a booleano (true si es "1", false si es "0").
|
||||||
|
* - default_taxes se divide por ";" y se filtran los valores vacíos o "#".
|
||||||
|
*
|
||||||
|
* @params dto - datos del cliente desde la API.
|
||||||
|
* @param context - Contexto adicional opcional para la adaptación.
|
||||||
|
* @returns {Customer} Objeto adaptado a Customer.
|
||||||
|
*/
|
||||||
|
|
||||||
export const GetCustomerByIdAdapter = {
|
export const GetCustomerByIdAdapter = {
|
||||||
fromDTO(
|
fromDTO(
|
||||||
dto: CustomerGetOutput | CustomerCreationResponseDTO | CustomerUpdateOutput,
|
dto: GetCustomerByIdResult | CustomerCreationResponseDTO | UpdateCustomerByIdResult,
|
||||||
context?: unknown
|
context?: unknown
|
||||||
): Customer {
|
): Customer {
|
||||||
const taxesAdapter = (taxes: string) =>
|
const taxesAdapter = (taxes: string) =>
|
||||||
|
|||||||
@ -1,8 +1,20 @@
|
|||||||
import type { CustomerListOutput } from "../api";
|
import type { ListCustomersResult } from "../api";
|
||||||
import type { CustomerList, CustomerListRow } from "../entities";
|
import type { CustomerList, CustomerListRow } from "../entities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adaptador para transformar los datos de la API de ListCustomersResult
|
||||||
|
* a la entidad CustomerList utilizada en la aplicación.
|
||||||
|
* Reglas de adaptación:
|
||||||
|
* - page, per_page, total_pages, total_items se asignan directamente.
|
||||||
|
* - items se transforma utilizando ListCustomersRowAdapter para cada elemento.
|
||||||
|
*
|
||||||
|
* @param pageDto - lista de clientes desde la API.
|
||||||
|
* @param context - Contexto adicional opcional para la adaptación.
|
||||||
|
* @returns {CustomerList} Objeto adaptado a CustomerList.
|
||||||
|
*/
|
||||||
|
|
||||||
export const ListCustomersAdapter = {
|
export const ListCustomersAdapter = {
|
||||||
fromDTO(pageDto: CustomerListOutput, context?: unknown): CustomerList {
|
fromDTO(pageDto: ListCustomersResult, context?: unknown): CustomerList {
|
||||||
return {
|
return {
|
||||||
page: pageDto.page,
|
page: pageDto.page,
|
||||||
per_page: pageDto.per_page,
|
per_page: pageDto.per_page,
|
||||||
@ -13,7 +25,18 @@ export const ListCustomersAdapter = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type CustomerListItemOutput = CustomerListOutput["items"][number];
|
/**
|
||||||
|
* Adaptador para transformar los items de la API de ListCustomersResult a la entidad CustomerListRow.
|
||||||
|
* Reglas de adaptación:
|
||||||
|
* - id, company_id, status, reference se asignan directamente.
|
||||||
|
* - is_company se convierte a booleano (true si es "1", false si es "0").
|
||||||
|
*
|
||||||
|
* @param rowDto - item de cliente desde la API.
|
||||||
|
* @param context - Contexto adicional opcional para la adaptación.
|
||||||
|
* @returns {CustomerListRow} Objeto adaptado a CustomerListRow.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type CustomerListItemOutput = ListCustomersResult["items"][number];
|
||||||
|
|
||||||
const ListCustomersRowAdapter = {
|
const ListCustomersRowAdapter = {
|
||||||
fromDTO(rowDto: CustomerListItemOutput, context?: unknown): CustomerListRow {
|
fromDTO(rowDto: CustomerListItemOutput, context?: unknown): CustomerListRow {
|
||||||
|
|||||||
@ -2,13 +2,21 @@ import type { IDataSource } from "@erp/core/client";
|
|||||||
|
|
||||||
import type { CreateCustomerRequestDTO, CustomerCreationResponseDTO } from "../../../common";
|
import type { CreateCustomerRequestDTO, CustomerCreationResponseDTO } from "../../../common";
|
||||||
|
|
||||||
export type CustomerCreateInput = CreateCustomerRequestDTO;
|
/**
|
||||||
export type CustomerCreateOutput = CustomerCreationResponseDTO;
|
* Crea un nuevo cliente en el sistema utilizando la fuente de datos proporcionada.
|
||||||
|
*
|
||||||
|
* @param dataSource - La fuente de datos para interactuar con la API.
|
||||||
|
* @param params - Los parámetros necesarios para crear el cliente.
|
||||||
|
* @returns Una promesa que resuelve con los detalles del cliente creado.
|
||||||
|
* @throws Error si el ID del cliente no es proporcionado o si la creación falla.
|
||||||
|
*/
|
||||||
|
|
||||||
export function createCustomer(dataSource: IDataSource, id: string, data: CustomerCreateInput) {
|
export type CreateCustomerParams = CreateCustomerRequestDTO;
|
||||||
|
|
||||||
|
export type CreateCustomerResult = CustomerCreationResponseDTO;
|
||||||
|
|
||||||
|
export function createCustomer(dataSource: IDataSource, params: CreateCustomerParams) {
|
||||||
|
const { id } = params;
|
||||||
if (!id) throw new Error("customerId is required");
|
if (!id) throw new Error("customerId is required");
|
||||||
return dataSource.createOne<CustomerCreateInput, CustomerCreateOutput>("customers", {
|
return dataSource.createOne<CreateCustomerRequestDTO, CreateCustomerResult>("customers", params);
|
||||||
...data,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,29 @@
|
|||||||
import type { IDataSource } from "@erp/core/client";
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
import type { DeleteCustomerByIdRequestDTO } from "../../../common";
|
/**
|
||||||
export type CustomerDeleteInput = DeleteCustomerByIdRequestDTO;
|
* Elimina un cliente existente en el sistema utilizando la fuente de datos proporcionada.
|
||||||
|
*
|
||||||
|
* @param dataSource - La fuente de datos para interactuar con la API.
|
||||||
|
* @param params - Los parámetros necesarios para eliminar el cliente.
|
||||||
|
* @param signal - Un AbortSignal para cancelar la solicitud si es necesario.
|
||||||
|
* @returns Una promesa que se resuelve cuando el cliente ha sido eliminado exitosamente.
|
||||||
|
* @throws Error si el ID del cliente no es proporcionado o si la eliminación falla.
|
||||||
|
*/
|
||||||
|
|
||||||
export function deleteCustomerById(dataSource: IDataSource, id: string, signal: AbortSignal) {
|
export type DeleteCustomerByIdParams = {
|
||||||
return dataSource.deleteOne<CustomerDeleteInput>("customers", id, { signal });
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteCustomerByIdResult = void;
|
||||||
|
|
||||||
|
export function deleteCustomerById(
|
||||||
|
dataSource: IDataSource,
|
||||||
|
params: DeleteCustomerByIdParams,
|
||||||
|
signal?: AbortSignal
|
||||||
|
): Promise<DeleteCustomerByIdResult> {
|
||||||
|
const { id } = params;
|
||||||
|
if (!id) throw new Error("customerId is required");
|
||||||
|
return dataSource.deleteOne<DeleteCustomerByIdResult>("customers", id, {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,28 @@ import type { IDataSource } from "@erp/core/client";
|
|||||||
|
|
||||||
import type { GetCustomerByIdResponseDTO } from "../../../common";
|
import type { GetCustomerByIdResponseDTO } from "../../../common";
|
||||||
|
|
||||||
export type CustomerGetOutput = GetCustomerByIdResponseDTO;
|
/**
|
||||||
|
* Recupera los detalles de un cliente específico utilizando su ID a través de la fuente de datos proporcionada.
|
||||||
|
*
|
||||||
|
* @param dataSource - La fuente de datos para interactuar con la API.
|
||||||
|
* @param params - Los parámetros necesarios para obtener el cliente, incluyendo su ID.
|
||||||
|
* @param signal - Un AbortSignal para cancelar la solicitud si es necesario.
|
||||||
|
* @returns Una promesa que resuelve con los detalles del cliente solicitado.
|
||||||
|
* @throws Error si el ID del cliente no es proporcionado o si la recuperación falla.
|
||||||
|
*/
|
||||||
|
|
||||||
export function getCustomerById(dataSource: IDataSource, id: string, signal: AbortSignal) {
|
export type GetCustomerByIdParams = {
|
||||||
return dataSource.getOne<CustomerGetOutput>("customers", id, { signal });
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetCustomerByIdResult = GetCustomerByIdResponseDTO;
|
||||||
|
|
||||||
|
export function getCustomerById(
|
||||||
|
dataSource: IDataSource,
|
||||||
|
params: GetCustomerByIdParams,
|
||||||
|
signal?: AbortSignal
|
||||||
|
) {
|
||||||
|
const { id } = params;
|
||||||
|
if (!id) throw new Error("customerId is required");
|
||||||
|
return dataSource.getOne<GetCustomerByIdResult>("customers", id, { signal });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
import type { CriteriaDTO } from "@erp/core";
|
||||||
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
|
import type { ListCustomersResponseDTO } from "../../../common";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recupera una lista de clientes del sistema utilizando la
|
||||||
|
* fuente de datos proporcionada y los criterios de búsqueda especificados.
|
||||||
|
*
|
||||||
|
* @param dataSource - La fuente de datos para interactuar con la API.
|
||||||
|
* @param params - Los parámetros necesarios para listar los clientes, incluyendo los criterios de búsqueda.
|
||||||
|
* @param signal - Un AbortSignal para cancelar la solicitud si es necesario.
|
||||||
|
* @returns Una promesa que resuelve con una lista de clientes que cumplen con los criterios especificados.
|
||||||
|
* @throws Error si la recuperación de la lista de clientes falla.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ListCustomersByCriteriaParams = {
|
||||||
|
criteria: CriteriaDTO;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListCustomersResult = ListCustomersResponseDTO;
|
||||||
|
|
||||||
|
export function getListCustomersByCriteria(
|
||||||
|
dataSource: IDataSource,
|
||||||
|
params: ListCustomersByCriteriaParams,
|
||||||
|
signal?: AbortSignal
|
||||||
|
): Promise<ListCustomersResult> {
|
||||||
|
const { criteria } = params;
|
||||||
|
return dataSource.getList<ListCustomersResult>("customers", {
|
||||||
|
signal,
|
||||||
|
...criteria,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
export * from "./create-customer.api";
|
export * from "./create-customer.api";
|
||||||
export * from "./delete-customer-by-id.api";
|
export * from "./delete-customer-by-id.api";
|
||||||
export * from "./get-customer-by-id.api";
|
export * from "./get-customer-by-id.api";
|
||||||
export * from "./list-customers.api";
|
export * from "./get-list-customers-by-criteria.api";
|
||||||
export * from "./update-customer-by-id.api";
|
export * from "./update-customer-by-id.api";
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
import type { CriteriaDTO } from "@erp/core";
|
|
||||||
import type { IDataSource } from "@erp/core/client";
|
|
||||||
|
|
||||||
import type { ListCustomersResponseDTO } from "../../../common";
|
|
||||||
|
|
||||||
export type CustomerListOutput = ListCustomersResponseDTO;
|
|
||||||
|
|
||||||
export function getListCustomers(
|
|
||||||
dataSource: IDataSource,
|
|
||||||
criteria: CriteriaDTO,
|
|
||||||
signal: AbortSignal
|
|
||||||
) {
|
|
||||||
return dataSource.getList<CustomerListOutput>("customers", {
|
|
||||||
signal,
|
|
||||||
...criteria,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -2,10 +2,32 @@ import type { IDataSource } from "@erp/core/client";
|
|||||||
|
|
||||||
import type { UpdateCustomerByIdRequestDTO, UpdateCustomerByIdResponseDTO } from "../../../common";
|
import type { UpdateCustomerByIdRequestDTO, UpdateCustomerByIdResponseDTO } from "../../../common";
|
||||||
|
|
||||||
export type CustomerUpdateInput = UpdateCustomerByIdRequestDTO;
|
/**
|
||||||
export type CustomerUpdateOutput = UpdateCustomerByIdResponseDTO;
|
* Actualiza un cliente existente en el sistema utilizando la fuente de datos proporcionada.
|
||||||
|
*
|
||||||
|
* @param dataSource - La fuente de datos para interactuar con la API.
|
||||||
|
* @param params - Los parámetros necesarios para actualizar el cliente.
|
||||||
|
* @returns Una promesa que resuelve con los detalles del cliente actualizado.
|
||||||
|
* @throws Error si el ID del cliente no es proporcionado o si la actualización falla.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type UpdateCustomerByIdParams = {
|
||||||
|
id: string;
|
||||||
|
data: UpdateCustomerByIdRequestDTO;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateCustomerByIdResult = UpdateCustomerByIdResponseDTO;
|
||||||
|
|
||||||
|
export function updateCustomerById(
|
||||||
|
dataSource: IDataSource,
|
||||||
|
params: UpdateCustomerByIdParams
|
||||||
|
): Promise<UpdateCustomerByIdResult> {
|
||||||
|
const { id, data } = params;
|
||||||
|
|
||||||
export function updateCustomerById(dataSource: IDataSource, id: string, data: CustomerUpdateInput) {
|
|
||||||
if (!id) throw new Error("customerId is required");
|
if (!id) throw new Error("customerId is required");
|
||||||
return dataSource.updateOne<CustomerUpdateInput, CustomerUpdateOutput>("customers", id, data);
|
return dataSource.updateOne<UpdateCustomerByIdRequestDTO, UpdateCustomerByIdResult>(
|
||||||
|
"customers",
|
||||||
|
id,
|
||||||
|
data
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Interface que representa una fila en la lista de clientes, adaptada desde la respuesta de la API.
|
||||||
|
* Contiene solo ciertos campos básicos del cliente.
|
||||||
|
*
|
||||||
|
*/
|
||||||
export interface CustomerListRow {
|
export interface CustomerListRow {
|
||||||
id: string;
|
id: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
import type { CustomerListRow } from "./customer-list-row.entity";
|
import type { CustomerListRow } from "./customer-list-row.entity";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface que representa la respuesta paginada de una lista de clientes,
|
||||||
|
* adaptada desde la respuesta de la API.
|
||||||
|
*/
|
||||||
|
|
||||||
export interface CustomerList {
|
export interface CustomerList {
|
||||||
items: CustomerListRow[];
|
items: CustomerListRow[];
|
||||||
total_pages: number;
|
total_pages: number;
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Interface que representa un cliente en el sistema,
|
||||||
|
* adaptada desde la respuesta de la API.
|
||||||
|
* Contiene todos los campos detallados del cliente.
|
||||||
|
*/
|
||||||
|
|
||||||
export interface Customer {
|
export interface Customer {
|
||||||
id: string;
|
id: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
|
|||||||
@ -5,6 +5,12 @@ import type { Customer, CustomerList, CustomerListRow } from "../entities";
|
|||||||
|
|
||||||
import { CUSTOMER_QUERY_KEY, LIST_CUSTOMERS_QUERY_KEY_PREFIX } from "./keys";
|
import { CUSTOMER_QUERY_KEY, LIST_CUSTOMERS_QUERY_KEY_PREFIX } from "./keys";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estrategias de caché para las consultas relacionadas con clientes.
|
||||||
|
* Incluye funciones para cancelar, invalidar, actualizar y eliminar cachés de clientes,
|
||||||
|
* así como para manejar actualizaciones optimistas al eliminar clientes.
|
||||||
|
*/
|
||||||
|
|
||||||
export interface CustomerListCacheSnapshot {
|
export interface CustomerListCacheSnapshot {
|
||||||
key: QueryKey;
|
key: QueryKey;
|
||||||
page?: CustomerList;
|
page?: CustomerList;
|
||||||
|
|||||||
@ -2,4 +2,4 @@ export * from "./use-customer-create-mutation";
|
|||||||
export * from "./use-customer-delete-mutation";
|
export * from "./use-customer-delete-mutation";
|
||||||
export * from "./use-customer-get-query";
|
export * from "./use-customer-get-query";
|
||||||
export * from "./use-customer-update-mutation";
|
export * from "./use-customer-update-mutation";
|
||||||
export * from "./use-list-customers-query";
|
export * from "./use-customers-list-query";
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import type { ZodError } from "zod";
|
import type { ZodError } from "zod";
|
||||||
|
|
||||||
// Helpers de validación a errores de dominio
|
/**
|
||||||
|
* Convierte un error de validación de Zod en una colección de errores de validación personalizada.
|
||||||
|
*
|
||||||
|
* @param error
|
||||||
|
* @returns array de objetos con el campo y el mensaje de error correspondiente.
|
||||||
|
*/
|
||||||
|
|
||||||
export function toValidationErrors(error: ZodError<unknown>) {
|
export function toValidationErrors(error: ZodError<unknown>) {
|
||||||
return error.issues.map((err) => ({
|
return error.issues.map((err) => ({
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-
|
|||||||
|
|
||||||
import { CreateCustomerRequestSchema } from "../../../common";
|
import { CreateCustomerRequestSchema } from "../../../common";
|
||||||
import { GetCustomerByIdAdapter } from "../adapters";
|
import { GetCustomerByIdAdapter } from "../adapters";
|
||||||
import { type CustomerCreateInput, createCustomer } from "../api";
|
import { type CreateCustomerParams, type CreateCustomerResult, createCustomer } from "../api";
|
||||||
import type { Customer } from "../entities";
|
import type { Customer } from "../entities";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -21,18 +21,19 @@ export const useCustomerCreateMutation = () => {
|
|||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const schema = CreateCustomerRequestSchema;
|
const schema = CreateCustomerRequestSchema;
|
||||||
|
|
||||||
return useMutation<Customer, DefaultError, CustomerCreateInput, CreateCustomerContext>({
|
return useMutation<Customer, DefaultError, CreateCustomerParams, CreateCustomerContext>({
|
||||||
mutationKey: CUSTOMER_CREATE_KEY,
|
mutationKey: CUSTOMER_CREATE_KEY,
|
||||||
|
|
||||||
mutationFn: async (payload) => {
|
mutationFn: async (payload) => {
|
||||||
const id = UniqueID.generateNewID().toString();
|
const id = UniqueID.generateNewID().toString();
|
||||||
|
const payloadWithId: CreateCustomerParams = { ...payload, id };
|
||||||
|
|
||||||
const result = schema.safeParse(payload);
|
const result = schema.safeParse(payloadWithId);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
const dto = await createCustomer(dataSource, id, payload);
|
const dto: CreateCustomerResult = await createCustomer(dataSource, payloadWithId);
|
||||||
return GetCustomerByIdAdapter.fromDTO(dto);
|
return GetCustomerByIdAdapter.fromDTO(dto);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { deleteCustomerById } from "../api";
|
import {
|
||||||
|
type DeleteCustomerByIdParams,
|
||||||
|
type DeleteCustomerByIdResult,
|
||||||
|
deleteCustomerById,
|
||||||
|
} from "../api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type DeleteCustomerCacheContext,
|
type DeleteCustomerCacheContext,
|
||||||
@ -12,25 +16,25 @@ import {
|
|||||||
} from "./customer-cache-strategy";
|
} from "./customer-cache-strategy";
|
||||||
import { CUSTOMER_DELETE_KEY } from "./keys";
|
import { CUSTOMER_DELETE_KEY } from "./keys";
|
||||||
|
|
||||||
export interface DeleteCustomerPayload {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteCustomerContext extends DeleteCustomerCacheContext {}
|
interface DeleteCustomerContext extends DeleteCustomerCacheContext {}
|
||||||
|
|
||||||
export const useCustomerDeleteMutation = () => {
|
export const useCustomerDeleteMutation = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
|
|
||||||
return useMutation<{ id: string }, DefaultError, DeleteCustomerPayload, DeleteCustomerContext>({
|
return useMutation<
|
||||||
|
DeleteCustomerByIdResult,
|
||||||
|
DefaultError,
|
||||||
|
DeleteCustomerByIdParams,
|
||||||
|
DeleteCustomerContext
|
||||||
|
>({
|
||||||
mutationKey: CUSTOMER_DELETE_KEY,
|
mutationKey: CUSTOMER_DELETE_KEY,
|
||||||
mutationFn: async ({ id }) => {
|
mutationFn: async ({ id }) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw new Error("customerId is required");
|
throw new Error("customerId is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteCustomerById(dataSource, id, new AbortController().signal);
|
await deleteCustomerById(dataSource, { id }, new AbortController().signal);
|
||||||
return { id };
|
|
||||||
},
|
},
|
||||||
onMutate: async ({ id }) => {
|
onMutate: async ({ id }) => {
|
||||||
return prepareDeleteCustomerOptimisticUpdate(queryClient, id);
|
return prepareDeleteCustomerOptimisticUpdate(queryClient, id);
|
||||||
@ -40,8 +44,8 @@ export const useCustomerDeleteMutation = () => {
|
|||||||
rollbackDeleteCustomerOptimisticUpdate(queryClient, context);
|
rollbackDeleteCustomerOptimisticUpdate(queryClient, context);
|
||||||
},
|
},
|
||||||
|
|
||||||
onSuccess: ({ id }) => {
|
onSuccess: (_, variables) => {
|
||||||
finalizeDeletedCustomerCaches(queryClient, id);
|
finalizeDeletedCustomerCaches(queryClient, variables.id);
|
||||||
},
|
},
|
||||||
onSettled: async () => {
|
onSettled: async () => {
|
||||||
await invalidateCustomerListQueries(queryClient);
|
await invalidateCustomerListQueries(queryClient);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useDataSource } from "@erp/core/hooks";
|
|||||||
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
|
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { GetCustomerByIdAdapter } from "../adapters";
|
import { GetCustomerByIdAdapter } from "../adapters";
|
||||||
import { getCustomerById } from "../api";
|
import { type GetCustomerByIdResult, getCustomerById } from "../api";
|
||||||
import type { Customer } from "../entities";
|
import type { Customer } from "../entities";
|
||||||
|
|
||||||
import { CUSTOMER_QUERY_KEY } from "./keys";
|
import { CUSTOMER_QUERY_KEY } from "./keys";
|
||||||
@ -22,7 +22,11 @@ export const useCustomerGetQuery = (
|
|||||||
return useQuery<Customer, DefaultError>({
|
return useQuery<Customer, DefaultError>({
|
||||||
queryKey: CUSTOMER_QUERY_KEY(customerId),
|
queryKey: CUSTOMER_QUERY_KEY(customerId),
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
const dto = await getCustomerById(dataSource, String(customerId), signal);
|
const dto: GetCustomerByIdResult = await getCustomerById(
|
||||||
|
dataSource,
|
||||||
|
{ id: customerId! },
|
||||||
|
signal
|
||||||
|
);
|
||||||
return GetCustomerByIdAdapter.fromDTO(dto);
|
return GetCustomerByIdAdapter.fromDTO(dto);
|
||||||
},
|
},
|
||||||
enabled,
|
enabled,
|
||||||
|
|||||||
@ -4,7 +4,11 @@ import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-
|
|||||||
|
|
||||||
import { UpdateCustomerByIdRequestSchema } from "../../../common";
|
import { UpdateCustomerByIdRequestSchema } from "../../../common";
|
||||||
import { GetCustomerByIdAdapter } from "../adapters";
|
import { GetCustomerByIdAdapter } from "../adapters";
|
||||||
import { type CustomerUpdateInput, updateCustomerById } from "../api";
|
import {
|
||||||
|
type UpdateCustomerByIdParams,
|
||||||
|
type UpdateCustomerByIdResult,
|
||||||
|
updateCustomerById,
|
||||||
|
} from "../api";
|
||||||
import type { Customer } from "../entities";
|
import type { Customer } from "../entities";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -16,17 +20,12 @@ import { toValidationErrors } from "./to-validation-errors";
|
|||||||
|
|
||||||
type UpdateCustomerContext = {};
|
type UpdateCustomerContext = {};
|
||||||
|
|
||||||
type UpdateCustomerPayload = {
|
|
||||||
id: string;
|
|
||||||
data: CustomerUpdateInput;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCustomerUpdateMutation = () => {
|
export const useCustomerUpdateMutation = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const schema = UpdateCustomerByIdRequestSchema;
|
const schema = UpdateCustomerByIdRequestSchema;
|
||||||
|
|
||||||
return useMutation<Customer, DefaultError, UpdateCustomerPayload, UpdateCustomerContext>({
|
return useMutation<Customer, DefaultError, UpdateCustomerByIdParams, UpdateCustomerContext>({
|
||||||
mutationKey: CUSTOMER_UPDATE_KEY,
|
mutationKey: CUSTOMER_UPDATE_KEY,
|
||||||
|
|
||||||
mutationFn: async (payload) => {
|
mutationFn: async (payload) => {
|
||||||
@ -40,7 +39,10 @@ export const useCustomerUpdateMutation = () => {
|
|||||||
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
const dto = await updateCustomerById(dataSource, customerId, data);
|
const dto: UpdateCustomerByIdResult = await updateCustomerById(
|
||||||
|
dataSource,
|
||||||
|
payload as UpdateCustomerByIdParams
|
||||||
|
);
|
||||||
return GetCustomerByIdAdapter.fromDTO(dto);
|
return GetCustomerByIdAdapter.fromDTO(dto);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -3,18 +3,18 @@ import { useDataSource } from "@erp/core/hooks";
|
|||||||
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
|
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { ListCustomersAdapter } from "../adapters";
|
import { ListCustomersAdapter } from "../adapters";
|
||||||
import { getListCustomers } from "../api";
|
import { type ListCustomersResult, getListCustomersByCriteria } from "../api";
|
||||||
import type { CustomerList } from "../entities";
|
import type { CustomerList } from "../entities";
|
||||||
|
|
||||||
import { LIST_CUSTOMERS_QUERY_KEY } from "./keys";
|
import { LIST_CUSTOMERS_QUERY_KEY } from "./keys";
|
||||||
|
|
||||||
type ListCustomersQueryOptions = {
|
type CustomerListQueryOptions = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
criteria?: Partial<CriteriaDTO>;
|
criteria?: Partial<CriteriaDTO>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useListCustomersQuery = (
|
export const useCustomerListQuery = (
|
||||||
options?: ListCustomersQueryOptions
|
options?: CustomerListQueryOptions
|
||||||
): UseQueryResult<CustomerList, DefaultError> => {
|
): UseQueryResult<CustomerList, DefaultError> => {
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const enabled = options?.enabled ?? true;
|
const enabled = options?.enabled ?? true;
|
||||||
@ -23,7 +23,11 @@ export const useListCustomersQuery = (
|
|||||||
return useQuery<CustomerList, DefaultError>({
|
return useQuery<CustomerList, DefaultError>({
|
||||||
queryKey: LIST_CUSTOMERS_QUERY_KEY(criteria),
|
queryKey: LIST_CUSTOMERS_QUERY_KEY(criteria),
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
const dto = await getListCustomers(dataSource, criteria as CriteriaDTO, signal);
|
const dto: ListCustomersResult = await getListCustomersByCriteria(
|
||||||
|
dataSource,
|
||||||
|
{ criteria },
|
||||||
|
signal
|
||||||
|
);
|
||||||
return ListCustomersAdapter.fromDTO(dto);
|
return ListCustomersAdapter.fromDTO(dto);
|
||||||
},
|
},
|
||||||
enabled,
|
enabled,
|
||||||
@ -3,8 +3,6 @@ import type { CustomerUpdateForm } from "../entities";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapea un cliente a un formulario de actualización de cliente.
|
* Mapea un cliente a un formulario de actualización de cliente.
|
||||||
* Es decir, adapta el shape de datos del dominio al shape de datos
|
|
||||||
* que necesita la UI para mostrar el formulario de actualización.
|
|
||||||
*
|
*
|
||||||
* @param customer
|
* @param customer
|
||||||
* @returns
|
* @returns
|
||||||
|
|||||||
@ -5,21 +5,25 @@ import { useEffect, useId, useMemo } from "react";
|
|||||||
import type { FieldErrors } from "react-hook-form";
|
import type { FieldErrors } from "react-hook-form";
|
||||||
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { type Customer, useCustomerGetQuery, useCustomerUpdateMutation } from "../../shared";
|
import {
|
||||||
|
type Customer,
|
||||||
|
type UpdateCustomerByIdParams,
|
||||||
|
useCustomerGetQuery,
|
||||||
|
useCustomerUpdateMutation,
|
||||||
|
} from "../../shared";
|
||||||
import { mapCustomerToCustomerUpdateForm } from "../adapters";
|
import { mapCustomerToCustomerUpdateForm } from "../adapters";
|
||||||
import {
|
import {
|
||||||
type CustomerUpdateForm,
|
type CustomerUpdateForm,
|
||||||
CustomerUpdateFormSchema,
|
CustomerUpdateFormSchema,
|
||||||
type CustomerUpdatePatch,
|
|
||||||
defaultCustomerUpdateForm,
|
defaultCustomerUpdateForm,
|
||||||
} from "../entities";
|
} from "../entities";
|
||||||
import { buildCustomerUpdatePatch } from "../utils";
|
import { buildCustomerUpdatePatch, buildUpdateCustomerByIdParams } from "../utils";
|
||||||
|
|
||||||
export interface UseCustomerUpdateControllerOptions {
|
export interface UseCustomerUpdateControllerOptions {
|
||||||
onUpdated?(updated: Customer): void;
|
onUpdated?(updated: Customer): void;
|
||||||
successToasts?: boolean; // mostrar o no toast automáticamente
|
successToasts?: boolean; // mostrar o no toast automáticamente
|
||||||
|
|
||||||
onError?(error: Error, patchData: CustomerUpdatePatch): void;
|
onError?(error: Error, params: UpdateCustomerByIdParams): void;
|
||||||
errorToasts?: boolean; // mostrar o no toast automáticamente
|
errorToasts?: boolean; // mostrar o no toast automáticamente
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,12 +97,14 @@ export const useCustomerUpdateController = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchData: CustomerUpdatePatch = buildCustomerUpdatePatch(formData, dirtyFields);
|
|
||||||
const previousData = customerData;
|
const previousData = customerData;
|
||||||
|
const patchData = buildCustomerUpdatePatch(formData, dirtyFields);
|
||||||
|
|
||||||
|
const params: UpdateCustomerByIdParams = buildUpdateCustomerByIdParams(customerId, patchData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Enviamos cambios al servidor
|
// Enviamos cambios al servidor
|
||||||
const updated = await mutateAsync({ id: customerId, data: patchData });
|
const updated = await mutateAsync(params);
|
||||||
|
|
||||||
// Ha ido bien -> actualizamos form con datos reales
|
// Ha ido bien -> actualizamos form con datos reales
|
||||||
// keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render.
|
// keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render.
|
||||||
@ -127,7 +133,7 @@ export const useCustomerUpdateController = (
|
|||||||
showErrorToast(t("pages.update.error.title"), normalizedError.message);
|
showErrorToast(t("pages.update.error.title"), normalizedError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
options?.onError?.(normalizedError, patchData);
|
options?.onError?.(normalizedError, params);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
(errors: FieldErrors<CustomerUpdateForm>) => {
|
(errors: FieldErrors<CustomerUpdateForm>) => {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
* CustomerUpdateForm representa el shape de datos del formulario de actualización de cliente.
|
* CustomerUpdateForm representa el shape de datos del formulario de actualización de cliente.
|
||||||
* Es decir, los campos que se muestran en el formulario y que el usuario puede editar.
|
* Es decir, los campos que se muestran en el formulario y que el usuario puede editar.
|
||||||
*
|
*
|
||||||
* Este shape es específico para la UI y no tiene por qué coincidir
|
* Es específico de la UI y no tiene por qué coincidir
|
||||||
* con el shape del dominio ni con el de la API.
|
* con el shape del dominio ni con el de la API.
|
||||||
*
|
*
|
||||||
* Debe cumplir las siguientes reglas:
|
* Debe cumplir las siguientes reglas:
|
||||||
|
|||||||
@ -15,33 +15,33 @@ import { z } from "zod/v4";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const CustomerUpdateFormSchema = z.object({
|
export const CustomerUpdateFormSchema = z.object({
|
||||||
reference: z.string().optional().or(z.literal("")),
|
reference: z.string(),
|
||||||
isCompany: z.boolean(),
|
isCompany: z.boolean(),
|
||||||
name: z.string().min(1, "El nombre es obligatorio"),
|
name: z.string().min(1, "El nombre es obligatorio"),
|
||||||
tradeName: z.string().optional().or(z.literal("")),
|
tradeName: z.string(),
|
||||||
tin: z.string(),
|
tin: z.string(),
|
||||||
|
|
||||||
defaultTaxes: z.array(z.string()),
|
defaultTaxes: z.array(z.string()),
|
||||||
|
|
||||||
street: z.string().optional().or(z.literal("")),
|
street: z.string(),
|
||||||
street2: z.string().optional().or(z.literal("")),
|
street2: z.string(),
|
||||||
city: z.string().optional().or(z.literal("")),
|
city: z.string(),
|
||||||
province: z.string().optional().or(z.literal("")),
|
province: z.string(),
|
||||||
postalCode: z.string().optional().or(z.literal("")),
|
postalCode: z.string(),
|
||||||
country: z.string().min(1, "El país es obligatorio").optional().or(z.literal("")),
|
country: z.string().min(1, "El país es obligatorio"),
|
||||||
|
|
||||||
primaryEmail: z.email("Email inválido").optional().or(z.literal("")),
|
primaryEmail: z.email("Email inválido"),
|
||||||
secondaryEmail: z.email("Email inválido").optional().or(z.literal("")),
|
secondaryEmail: z.email("Email inválido"),
|
||||||
|
|
||||||
primaryPhone: z.string().optional().or(z.literal("")),
|
primaryPhone: z.string(),
|
||||||
secondaryPhone: z.string().optional().or(z.literal("")),
|
secondaryPhone: z.string(),
|
||||||
primaryMobile: z.string().optional().or(z.literal("")),
|
primaryMobile: z.string(),
|
||||||
secondaryMobile: z.string().optional().or(z.literal("")),
|
secondaryMobile: z.string(),
|
||||||
|
|
||||||
fax: z.string().optional().or(z.literal("")),
|
fax: z.string(),
|
||||||
website: z.url("URL inválida").optional().or(z.literal("")),
|
website: z.url("URL inválida"),
|
||||||
|
|
||||||
legalRecord: z.string().optional().or(z.literal("")),
|
legalRecord: z.string(),
|
||||||
|
|
||||||
languageCode: z.string().min(1, "El idioma es obligatorio"),
|
languageCode: z.string().min(1, "El idioma es obligatorio"),
|
||||||
currencyCode: z.string().min(1, "La moneda es obligatoria"),
|
currencyCode: z.string().min(1, "La moneda es obligatoria"),
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import type { CustomerUpdateForm } from "./customer-update-form.entity";
|
import type { CustomerUpdateForm } from "./customer-update-form.entity";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CustomerUpdatePatch es un tipo que representa un objeto con las mismas
|
* CustomerUpdatePatch representa los cambios que se van a aplicar a un cliente.
|
||||||
* propiedades que CustomerUpdateForm, pero todas ellas son opcionales.
|
* Se representa con las mismas propiedades que CustomerUpdateForm,
|
||||||
|
* pero todas ellas son opcionales.
|
||||||
*
|
*
|
||||||
* Esto es útil para representar los datos que se van a enviar a la API para actualizar un cliente,
|
* A la API solo hay que enviar los campos que han cambiado.
|
||||||
* ya que en una actualización parcial (PATCH) no es necesario enviar todos los campos,
|
|
||||||
* sino solo aquellos que se quieren modificar.
|
|
||||||
*
|
*
|
||||||
* Reglas:
|
* Reglas:
|
||||||
* - debe ser un Partial de CustomerUpdateForm
|
* - debe ser un Partial de CustomerUpdateForm
|
||||||
|
|||||||
@ -5,13 +5,17 @@ import { CustomerAddressFields } from "./customer-address-fields";
|
|||||||
import { CustomerBasicInfoFields } from "./customer-basic-info-fields";
|
import { CustomerBasicInfoFields } from "./customer-basic-info-fields";
|
||||||
import { CustomerContactFields } from "./customer-contact-fields";
|
import { CustomerContactFields } from "./customer-contact-fields";
|
||||||
|
|
||||||
type CustomerFormProps = {
|
type CustomerUpdateEditorFormProps = {
|
||||||
formId: string;
|
formId: string;
|
||||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CustomerEditForm = ({ formId, onSubmit, className }: CustomerFormProps) => {
|
export const CustomerUpdateEditorForm = ({
|
||||||
|
formId,
|
||||||
|
onSubmit,
|
||||||
|
className,
|
||||||
|
}: CustomerUpdateEditorFormProps) => {
|
||||||
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)}>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { FormProvider } from "react-hook-form";
|
|||||||
import { useTranslation } from "../../../i18n";
|
import { useTranslation } from "../../../i18n";
|
||||||
import { useCustomerUpdatePageController } from "../../controllers";
|
import { useCustomerUpdatePageController } from "../../controllers";
|
||||||
import { CustomerEditorSkeleton } from "../components";
|
import { CustomerEditorSkeleton } from "../components";
|
||||||
import { CustomerEditForm } from "../editor";
|
import { CustomerUpdateEditorForm } from "../editor";
|
||||||
|
|
||||||
export const CustomerUpdatePage = () => {
|
export const CustomerUpdatePage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -105,7 +105,7 @@ export const CustomerUpdatePage = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<CustomerEditForm
|
<CustomerUpdateEditorForm
|
||||||
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto mt-6 " // para que el botón del header pueda hacer submit
|
className="bg-white rounded-xl border shadow-xl max-w-7xl mx-auto mt-6 " // para que el botón del header pueda hacer submit
|
||||||
formId={formId}
|
formId={formId}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { pickFormDirtyValues } from "@erp/core/client";
|
||||||
|
import type { FieldNamesMarkedBoolean } from "react-hook-form";
|
||||||
|
|
||||||
|
import type { CustomerUpdateForm, CustomerUpdatePatch } from "../entities";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construye un parche de actualización de cliente a partir de los datos
|
||||||
|
* del formulario y los campos sucios.
|
||||||
|
*
|
||||||
|
* @param formData
|
||||||
|
* @param dirtyFields
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const buildCustomerUpdatePatch = (
|
||||||
|
formData: CustomerUpdateForm,
|
||||||
|
dirtyFields: FieldNamesMarkedBoolean<CustomerUpdateForm>
|
||||||
|
): CustomerUpdatePatch => {
|
||||||
|
return pickFormDirtyValues(formData, dirtyFields) as CustomerUpdatePatch;
|
||||||
|
};
|
||||||
@ -1,7 +1,5 @@
|
|||||||
import { pickFormDirtyValues } from "@erp/core/client";
|
import type { UpdateCustomerByIdParams } from "../../shared";
|
||||||
import type { FieldNamesMarkedBoolean } from "react-hook-form";
|
import type { CustomerUpdatePatch } from "../entities";
|
||||||
|
|
||||||
import type { CustomerUpdateForm, CustomerUpdatePatch } from "../entities";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construye un parche de actualización de cliente a partir de los datos
|
* Construye un parche de actualización de cliente a partir de los datos
|
||||||
@ -15,15 +13,22 @@ import type { CustomerUpdateForm, CustomerUpdatePatch } from "../entities";
|
|||||||
* - no debe tener transformaciones ni campos adicionales, solo los que vienen del
|
* - no debe tener transformaciones ni campos adicionales, solo los que vienen del
|
||||||
* formulario y están sucios.
|
* formulario y están sucios.
|
||||||
*
|
*
|
||||||
* @param formData
|
* @param id - El ID del cliente que se va a actualizar.
|
||||||
* @param dirtyFields
|
* @param patchData - Los datos del parche de actualización.
|
||||||
* @returns Un objeto que se puede enviar a la API para actualizar un cliente,
|
* @returns Un objeto que se puede enviar a la API para actualizar un cliente,
|
||||||
* con solo los campos que han cambiado.
|
* con solo los campos que han cambiado.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const buildCustomerUpdatePatch = (
|
export const buildUpdateCustomerByIdParams = (
|
||||||
formData: CustomerUpdateForm,
|
id: string,
|
||||||
dirtyFields: FieldNamesMarkedBoolean<CustomerUpdateForm>
|
patchData: CustomerUpdatePatch
|
||||||
): CustomerUpdatePatch => {
|
): UpdateCustomerByIdParams => {
|
||||||
return pickFormDirtyValues(formData, dirtyFields) as CustomerUpdatePatch;
|
if (!id) {
|
||||||
|
throw new Error("customerId is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
data: patchData,
|
||||||
|
} as UpdateCustomerByIdParams;
|
||||||
};
|
};
|
||||||
@ -1 +1,2 @@
|
|||||||
export * from "./build-customer-update-patch";
|
export * from "./build-customer.update-patch";
|
||||||
|
export * from "./build-update-customer-by-id-params";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user