Repaso de web "Customers"
This commit is contained in:
parent
699e097016
commit
f8b8dd9172
@ -1,40 +1,7 @@
|
|||||||
import { MetadataSchema } from "@erp/core";
|
import {
|
||||||
import { z } from "zod/v4";
|
type GetCustomerByIdResponseDTO,
|
||||||
|
GetCustomerByIdResponseSchema,
|
||||||
|
} from "./get-customer-by-id.response.dto";
|
||||||
|
|
||||||
export const UpdateCustomerByIdResponseSchema = z.object({
|
export const UpdateCustomerByIdResponseSchema = GetCustomerByIdResponseSchema;
|
||||||
id: z.uuid(),
|
export type UpdateCustomerByIdResponseDTO = GetCustomerByIdResponseDTO;
|
||||||
company_id: z.uuid(),
|
|
||||||
reference: z.string(),
|
|
||||||
|
|
||||||
is_company: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
trade_name: z.string(),
|
|
||||||
tin: z.string(),
|
|
||||||
|
|
||||||
street: z.string(),
|
|
||||||
street2: z.string(),
|
|
||||||
city: z.string(),
|
|
||||||
province: z.string(),
|
|
||||||
postal_code: z.string(),
|
|
||||||
country: z.string(),
|
|
||||||
|
|
||||||
email_primary: z.string(),
|
|
||||||
email_secondary: z.string(),
|
|
||||||
phone_primary: z.string(),
|
|
||||||
phone_secondary: z.string(),
|
|
||||||
mobile_primary: z.string(),
|
|
||||||
mobile_secondary: z.string(),
|
|
||||||
|
|
||||||
fax: z.string(),
|
|
||||||
website: z.string(),
|
|
||||||
|
|
||||||
legal_record: z.string(),
|
|
||||||
|
|
||||||
default_taxes: z.array(z.string()),
|
|
||||||
language_code: z.string(),
|
|
||||||
currency_code: z.string(),
|
|
||||||
|
|
||||||
metadata: MetadataSchema.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type UpdateCustomerByIdResponseDTO = z.infer<typeof UpdateCustomerByIdResponseSchema>;
|
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
export const COUNTRY_OPTIONS = [
|
|
||||||
{ value: "es", label: "España" },
|
|
||||||
{ value: "fr", label: "Francia" },
|
|
||||||
{ value: "de", label: "Alemania" },
|
|
||||||
{ value: "it", label: "Italia" },
|
|
||||||
{ value: "pt", label: "Portugal" },
|
|
||||||
{ value: "us", label: "Estados Unidos" },
|
|
||||||
{ value: "mx", label: "México" },
|
|
||||||
{ value: "ar", label: "Argentina" },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const LANGUAGE_OPTIONS = [
|
|
||||||
{ value: "es", label: "Español" },
|
|
||||||
{ value: "en", label: "Inglés" },
|
|
||||||
{ value: "fr", label: "Francés" },
|
|
||||||
{ value: "de", label: "Alemán" },
|
|
||||||
{ value: "it", label: "Italiano" },
|
|
||||||
{ value: "pt", label: "Portugués" },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const CURRENCY_OPTIONS = [
|
|
||||||
{ value: "EUR", label: "Euro" },
|
|
||||||
{ value: "USD", label: "Dólar estadounidense" },
|
|
||||||
{ value: "GBP", label: "Libra esterlina" },
|
|
||||||
{ value: "ARS", label: "Peso argentino" },
|
|
||||||
{ value: "MXN", label: "Peso mexicano" },
|
|
||||||
{ value: "JPY", label: "Yen japonés" },
|
|
||||||
] as const;
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./customer.constants";
|
|
||||||
@ -3,7 +3,7 @@ import { UniqueID, ValidationErrorCollection } from "@repo/rdx-ddd";
|
|||||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { CreateCustomerRequestSchema } from "../../../common";
|
import { CreateCustomerRequestSchema } from "../../../common";
|
||||||
import { toValidationErrors } from "../../shared/hooks/toValidationErrors";
|
import { toValidationErrors } from "../../shared/hooks/to-validation-errors";
|
||||||
import type { Customer, CustomerFormData } from "../schemas";
|
import type { Customer, CustomerFormData } from "../schemas";
|
||||||
|
|
||||||
import { CUSTOMERS_LIST_KEY, invalidateCustomerListCache } from "./use-customer-list-query";
|
import { CUSTOMERS_LIST_KEY, invalidateCustomerListCache } from "./use-customer-list-query";
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
|||||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { UpdateCustomerByIdRequestSchema } from "../../../common";
|
import { UpdateCustomerByIdRequestSchema } from "../../../common";
|
||||||
import { toValidationErrors } from "../../shared/hooks/toValidationErrors";
|
import { toValidationErrors } from "../../shared/hooks/to-validation-errors";
|
||||||
import type { Customer, CustomerFormData } from "../schemas";
|
import type { Customer, CustomerFormData } from "../schemas";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
1
modules/customers/src/web/create/controllers/index.ts
Normal file
1
modules/customers/src/web/create/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./use-customer-update-page.controller";
|
||||||
@ -0,0 +1,120 @@
|
|||||||
|
import { formHasAnyDirty, pickFormDirtyValues } from "@erp/core/client";
|
||||||
|
import { useHookForm } from "@erp/core/hooks";
|
||||||
|
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
||||||
|
import { useId } from "react";
|
||||||
|
import { type FieldErrors, FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../i18n";
|
||||||
|
import type { Customer } from "../../shared";
|
||||||
|
import { type CustomerFormData, CustomerFormSchema, defaultCustomerFormData } from "../types";
|
||||||
|
|
||||||
|
export interface UseCustomerCreateControllerOptions {
|
||||||
|
onCreated?(created: Customer): void;
|
||||||
|
successToasts?: boolean; // mostrar o no toast automáticcamente
|
||||||
|
|
||||||
|
onError?(error: Error, patchData: ReturnType<typeof pickFormDirtyValues>): void;
|
||||||
|
errorToasts?: boolean; // mostrar o no toast automáticcamente
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCustomerCreateController = (
|
||||||
|
customerId?: string,
|
||||||
|
options?: UseCustomerCreateControllerOptions
|
||||||
|
) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const formId = useId(); // id único por instancia
|
||||||
|
|
||||||
|
// 1) Estado de creación (mutación)
|
||||||
|
const {
|
||||||
|
mutateAsync,
|
||||||
|
isPending,
|
||||||
|
isError: isCreateError,
|
||||||
|
error: createError,
|
||||||
|
} = useCustomerCreateMutation();
|
||||||
|
|
||||||
|
// 2) Form hook
|
||||||
|
const form = useHookForm<CustomerFormData>({
|
||||||
|
resolverSchema: CustomerFormSchema,
|
||||||
|
initialValues: defaultCustomerFormData,
|
||||||
|
disabled: isPending,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Handlers */
|
||||||
|
|
||||||
|
const resetForm = () => form.reset(defaultCustomerFormData);
|
||||||
|
|
||||||
|
// Versión sincronizada
|
||||||
|
const submitHandler = form.handleSubmit(
|
||||||
|
async (formData) => {
|
||||||
|
if (!customerId) {
|
||||||
|
showErrorToast(t("pages.create.error.title"), "Falta el ID del cliente");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dirtyFields } = form.formState;
|
||||||
|
if (!formHasAnyDirty(dirtyFields)) {
|
||||||
|
showWarningToast(t("pages.create.error.no_changes"), "No hay cambios para guardar");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchData = pickFormDirtyValues(formData, dirtyFields);
|
||||||
|
const previousData = customerData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Enviamos cambios al servidor
|
||||||
|
const created = await mutateAsync({ id: customerId, data: patchData });
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
|
||||||
|
if (options?.successToasts !== false) {
|
||||||
|
showSuccessToast(
|
||||||
|
t("pages.create.success.title", "Cliente modificado"),
|
||||||
|
t("pages.create.success.message", "Se ha modificado correctamente.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
options?.onCreated?.(created);
|
||||||
|
} catch (error: any) {
|
||||||
|
// Algo ha fallado -> revertimos cambios
|
||||||
|
form.reset(previousData ?? defaultCustomerFormData);
|
||||||
|
if (options?.errorToasts !== false) {
|
||||||
|
showErrorToast(t("pages.create.error.title"), error.message);
|
||||||
|
}
|
||||||
|
options?.onError?.(error, patchData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(errors: FieldErrors<CustomerFormData>) => {
|
||||||
|
const firstKey = Object.keys(errors)[0] as keyof CustomerFormData | undefined;
|
||||||
|
if (firstKey) document.querySelector<HTMLElement>(`[name="${String(firstKey)}"]`)?.focus();
|
||||||
|
|
||||||
|
showWarningToast(
|
||||||
|
t("forms.validation.title", "Revisa los campos"),
|
||||||
|
t("forms.validation.message", "Hay errores de validación en el formulario.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
isPending,
|
||||||
|
isCreateError,
|
||||||
|
createError,
|
||||||
|
|
||||||
|
// Por comodidad
|
||||||
|
FormProvider,
|
||||||
|
};
|
||||||
|
};
|
||||||
1
modules/customers/src/web/create/types/index.ts
Normal file
1
modules/customers/src/web/create/types/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./types";
|
||||||
89
modules/customers/src/web/create/types/types.ts
Normal file
89
modules/customers/src/web/create/types/types.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const CustomerFormSchema = z.object({
|
||||||
|
reference: z.string().optional(),
|
||||||
|
|
||||||
|
is_company: z.string().default("true"),
|
||||||
|
name: z
|
||||||
|
.string({
|
||||||
|
error: "El nombre es obligatorio",
|
||||||
|
})
|
||||||
|
.min(1, "El nombre no puede estar vacío"),
|
||||||
|
trade_name: z.string().optional(),
|
||||||
|
tin: z.string().optional(),
|
||||||
|
default_taxes: z.array(z.string()).default([]),
|
||||||
|
|
||||||
|
street: z.string().optional(),
|
||||||
|
street2: z.string().optional(),
|
||||||
|
city: z.string().optional(),
|
||||||
|
province: z.string().optional(),
|
||||||
|
postal_code: 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_secondary: z.string().optional(),
|
||||||
|
phone_primary: z.string().optional(),
|
||||||
|
phone_secondary: z.string().optional(),
|
||||||
|
mobile_primary: z.string().optional(),
|
||||||
|
mobile_secondary: z.string().optional(),
|
||||||
|
|
||||||
|
fax: z.string().optional(),
|
||||||
|
website: z.string().optional(),
|
||||||
|
|
||||||
|
legal_record: z.string().optional(),
|
||||||
|
|
||||||
|
language_code: z
|
||||||
|
.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 const defaultCustomerFormData: CustomerFormData = {
|
||||||
|
reference: "",
|
||||||
|
|
||||||
|
is_company: "",
|
||||||
|
name: "",
|
||||||
|
trade_name: "",
|
||||||
|
tin: "",
|
||||||
|
default_taxes: [],
|
||||||
|
|
||||||
|
street: "",
|
||||||
|
street2: "",
|
||||||
|
city: "",
|
||||||
|
province: "",
|
||||||
|
postal_code: "",
|
||||||
|
country: "es",
|
||||||
|
|
||||||
|
email_primary: "",
|
||||||
|
email_secondary: "",
|
||||||
|
phone_primary: "",
|
||||||
|
phone_secondary: "",
|
||||||
|
mobile_primary: "",
|
||||||
|
mobile_secondary: "",
|
||||||
|
|
||||||
|
fax: "",
|
||||||
|
website: "",
|
||||||
|
|
||||||
|
legal_record: "",
|
||||||
|
language_code: "",
|
||||||
|
currency_code: "",
|
||||||
|
};
|
||||||
0
modules/customers/src/web/create/ui/editor/index.ts
Normal file
0
modules/customers/src/web/create/ui/editor/index.ts
Normal file
0
modules/customers/src/web/create/ui/forms/index.ts
Normal file
0
modules/customers/src/web/create/ui/forms/index.ts
Normal file
0
modules/customers/src/web/create/ui/index.ts
Normal file
0
modules/customers/src/web/create/ui/index.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { ErrorAlert, NotFoundCard, PageHeader } from "@erp/core/components";
|
||||||
|
import { CreateCommitButtonGroup, UnsavedChangesProvider, useUrlParamId } from "@erp/core/hooks";
|
||||||
|
import { AppContent, AppHeader, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../i18n";
|
||||||
|
import { useCustomerCreateController } from "../../controllers";
|
||||||
|
import { CustomerEditorSkeleton } from "../components";
|
||||||
|
import { CustomerEditForm } from "../editor";
|
||||||
|
|
||||||
|
export const CustomerCreatePage = () => {
|
||||||
|
const initialCustomerId = useUrlParamId();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formId,
|
||||||
|
onSubmit,
|
||||||
|
resetForm,
|
||||||
|
|
||||||
|
customerData,
|
||||||
|
isLoading,
|
||||||
|
isLoadError,
|
||||||
|
loadError,
|
||||||
|
|
||||||
|
isUpdating,
|
||||||
|
isCreateError,
|
||||||
|
createError,
|
||||||
|
|
||||||
|
FormProvider,
|
||||||
|
} = useCustomerCreateController(initialCustomerId, {});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <CustomerEditorSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoadError) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppContent>
|
||||||
|
<ErrorAlert
|
||||||
|
message={
|
||||||
|
(loadError as Error)?.message ??
|
||||||
|
t("pages.create.loadErrorMsg", "Inténtalo de nuevo más tarde.")
|
||||||
|
}
|
||||||
|
title={t("pages.create.loadErrorTitle", "No se pudo cargar el cliente")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<BackHistoryButton />
|
||||||
|
</div>
|
||||||
|
</AppContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customerData)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppContent>
|
||||||
|
<NotFoundCard
|
||||||
|
message={t("pages.create.notFoundMsg", "Revisa el identificador o vuelve al listado.")}
|
||||||
|
title={t("pages.create.notFoundTitle", "Cliente no encontrado")}
|
||||||
|
/>
|
||||||
|
</AppContent>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnsavedChangesProvider isDirty={false}>
|
||||||
|
<AppHeader>
|
||||||
|
<PageHeader
|
||||||
|
backIcon
|
||||||
|
description={t("pages.create.description")}
|
||||||
|
rightSlot={
|
||||||
|
<CreateCommitButtonGroup
|
||||||
|
cancel={{
|
||||||
|
formId,
|
||||||
|
to: "/customers/list",
|
||||||
|
disabled: isUpdating,
|
||||||
|
}}
|
||||||
|
disabled={isUpdating}
|
||||||
|
isLoading={isUpdating}
|
||||||
|
onReset={resetForm}
|
||||||
|
submit={{
|
||||||
|
formId,
|
||||||
|
disabled: isUpdating,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
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
modules/customers/src/web/create/ui/pages/index.ts
Normal file
1
modules/customers/src/web/create/ui/pages/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-create-page";
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import type { Customer } from "../entities/customer.entity";
|
||||||
|
import type { CustomerListRow } from "../entities/customer-list-row.entity";
|
||||||
|
|
||||||
|
export type CustomerListRowPatch = Pick<CustomerListRow, "id"> & Partial<CustomerListRow>;
|
||||||
|
|
||||||
|
export const CustomerToListRowPatchAdapter = {
|
||||||
|
fromCustomer(customer: Customer): CustomerListRowPatch {
|
||||||
|
return {
|
||||||
|
id: customer.id,
|
||||||
|
status: customer.status,
|
||||||
|
reference: customer.reference,
|
||||||
|
|
||||||
|
isCompany: customer.isCompany,
|
||||||
|
name: customer.name,
|
||||||
|
tradeName: customer.tradeName,
|
||||||
|
tin: customer.tin,
|
||||||
|
|
||||||
|
street: customer.street,
|
||||||
|
street2: customer.street2,
|
||||||
|
city: customer.city,
|
||||||
|
province: customer.province,
|
||||||
|
postalCode: customer.postalCode,
|
||||||
|
country: customer.country,
|
||||||
|
|
||||||
|
primaryEmail: customer.primaryEmail,
|
||||||
|
secondaryEmail: customer.secondaryEmail,
|
||||||
|
primaryPhone: customer.primaryPhone,
|
||||||
|
secondaryPhone: customer.secondaryPhone,
|
||||||
|
primaryMobile: customer.primaryMobile,
|
||||||
|
secondaryMobile: customer.secondaryMobile,
|
||||||
|
fax: customer.fax,
|
||||||
|
website: customer.website,
|
||||||
|
|
||||||
|
languageCode: customer.languageCode,
|
||||||
|
currencyCode: customer.currencyCode,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,9 +1,15 @@
|
|||||||
import type { GetCustomerByIdResponseDTO } from "../../../common";
|
import type { CustomerCreationResponseDTO } from "@erp/customers/common";
|
||||||
|
|
||||||
|
import type { CustomerGetOutput, CustomerUpdateOutput } from "../api";
|
||||||
import type { Customer } from "../entities";
|
import type { Customer } from "../entities";
|
||||||
|
|
||||||
export const GetCustomerByIdAdapter = {
|
export const GetCustomerByIdAdapter = {
|
||||||
fromDTO(dto: GetCustomerByIdResponseDTO, context?: unknown): Customer {
|
fromDTO(
|
||||||
const taxesAdapter = (taxes: string) => taxes.split(";").filter((item) => item !== "#") || [];
|
dto: CustomerGetOutput | CustomerCreationResponseDTO | CustomerUpdateOutput,
|
||||||
|
context?: unknown
|
||||||
|
): Customer {
|
||||||
|
const taxesAdapter = (taxes: string) =>
|
||||||
|
taxes.split(";").filter((item) => item !== "#" && item.trim() !== "");
|
||||||
|
|
||||||
const defaultTaxes = taxesAdapter(dto.default_taxes);
|
const defaultTaxes = taxesAdapter(dto.default_taxes);
|
||||||
|
|
||||||
@ -12,7 +18,7 @@ export const GetCustomerByIdAdapter = {
|
|||||||
companyId: dto.company_id,
|
companyId: dto.company_id,
|
||||||
reference: dto.reference,
|
reference: dto.reference,
|
||||||
|
|
||||||
isCompany: dto.is_company,
|
isCompany: dto.is_company === "1",
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
tradeName: dto.trade_name,
|
tradeName: dto.trade_name,
|
||||||
tin: dto.tin,
|
tin: dto.tin,
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
|
export * from "./customer-to-list-row-patch.adapter";
|
||||||
export * from "./get-customer-by-id.adapter";
|
export * from "./get-customer-by-id.adapter";
|
||||||
export * from "./list-customers.adapter";
|
export * from "./list-customers.adapter";
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
import type { ListCustomersResponseDTO } from "../../../common";
|
import type { CustomerListOutput } from "../api";
|
||||||
import type { CustomerList, CustomerListRow } from "../entities";
|
import type { CustomerList, CustomerListRow } from "../entities";
|
||||||
|
|
||||||
type ListCustomersItemDTO = ListCustomersResponseDTO["items"][number];
|
|
||||||
|
|
||||||
export const ListCustomersAdapter = {
|
export const ListCustomersAdapter = {
|
||||||
fromDTO(pageDto: ListCustomersResponseDTO, context?: unknown): CustomerList {
|
fromDTO(pageDto: CustomerListOutput, context?: unknown): CustomerList {
|
||||||
return {
|
return {
|
||||||
//...pageDto,
|
|
||||||
page: pageDto.page,
|
page: pageDto.page,
|
||||||
per_page: pageDto.per_page,
|
per_page: pageDto.per_page,
|
||||||
total_pages: pageDto.total_pages,
|
total_pages: pageDto.total_pages,
|
||||||
@ -16,13 +13,10 @@ export const ListCustomersAdapter = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListCustomersRowAdapter = {
|
type CustomerListItemOutput = CustomerListOutput["items"][number];
|
||||||
fromDTO(rowDto: ListCustomersItemDTO, context?: unknown): CustomerListRow {
|
|
||||||
/*return {
|
|
||||||
...rowDto,
|
|
||||||
is_company: rowDto.is_company === "1",
|
|
||||||
};*/
|
|
||||||
|
|
||||||
|
const ListCustomersRowAdapter = {
|
||||||
|
fromDTO(rowDto: CustomerListItemOutput, context?: unknown): CustomerListRow {
|
||||||
return {
|
return {
|
||||||
id: rowDto.id,
|
id: rowDto.id,
|
||||||
companyId: rowDto.company_id,
|
companyId: rowDto.company_id,
|
||||||
|
|||||||
14
modules/customers/src/web/shared/api/create-customer.api.ts
Normal file
14
modules/customers/src/web/shared/api/create-customer.api.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
|
import type { CreateCustomerRequestDTO, CustomerCreationResponseDTO } from "../../../common";
|
||||||
|
|
||||||
|
export type CustomerCreateInput = CreateCustomerRequestDTO;
|
||||||
|
export type CustomerCreateOutput = CustomerCreationResponseDTO;
|
||||||
|
|
||||||
|
export function createCustomer(dataSource: IDataSource, id: string, data: CustomerCreateInput) {
|
||||||
|
if (!id) throw new Error("customerId is required");
|
||||||
|
return dataSource.createOne<CustomerCreateInput, CustomerCreateOutput>("customers", {
|
||||||
|
...data,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
|
import type { DeleteCustomerByIdRequestDTO } from "../../../common";
|
||||||
|
export type CustomerDeleteInput = DeleteCustomerByIdRequestDTO;
|
||||||
|
|
||||||
|
export function deleteCustomerById(dataSource: IDataSource, id: string, signal: AbortSignal) {
|
||||||
|
return dataSource.deleteOne<CustomerDeleteInput>("customers", id, { signal });
|
||||||
|
}
|
||||||
@ -2,8 +2,8 @@ import type { IDataSource } from "@erp/core/client";
|
|||||||
|
|
||||||
import type { GetCustomerByIdResponseDTO } from "../../../common";
|
import type { GetCustomerByIdResponseDTO } from "../../../common";
|
||||||
|
|
||||||
export async function getCustomerById(dataSource: IDataSource, signal: AbortSignal, id?: string) {
|
export type CustomerGetOutput = GetCustomerByIdResponseDTO;
|
||||||
if (!id) throw new Error("customerId is required");
|
|
||||||
const response = dataSource.getOne<GetCustomerByIdResponseDTO>("customers", id, { signal });
|
export function getCustomerById(dataSource: IDataSource, id: string, signal: AbortSignal) {
|
||||||
return response;
|
return dataSource.getOne<CustomerGetOutput>("customers", id, { signal });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1,5 @@
|
|||||||
|
export * from "./create-customer.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 "./list-customers.api";
|
||||||
|
export * from "./update-customer-by-id.api";
|
||||||
|
|||||||
@ -3,15 +3,15 @@ import type { IDataSource } from "@erp/core/client";
|
|||||||
|
|
||||||
import type { ListCustomersResponseDTO } from "../../../common";
|
import type { ListCustomersResponseDTO } from "../../../common";
|
||||||
|
|
||||||
export async function getListCustomers(
|
export type CustomerListOutput = ListCustomersResponseDTO;
|
||||||
|
|
||||||
|
export function getListCustomers(
|
||||||
dataSource: IDataSource,
|
dataSource: IDataSource,
|
||||||
signal: AbortSignal,
|
criteria: CriteriaDTO,
|
||||||
criteria: CriteriaDTO
|
signal: AbortSignal
|
||||||
) {
|
) {
|
||||||
const response = dataSource.getList<ListCustomersResponseDTO>("customers", {
|
return dataSource.getList<CustomerListOutput>("customers", {
|
||||||
signal,
|
signal,
|
||||||
...criteria,
|
...criteria,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
|
import type { UpdateCustomerByIdRequestDTO, UpdateCustomerByIdResponseDTO } from "../../../common";
|
||||||
|
|
||||||
|
export type CustomerUpdateInput = UpdateCustomerByIdRequestDTO;
|
||||||
|
export type CustomerUpdateOutput = UpdateCustomerByIdResponseDTO;
|
||||||
|
|
||||||
|
export function updateCustomerById(dataSource: IDataSource, id: string, data: CustomerUpdateInput) {
|
||||||
|
if (!id) throw new Error("customerId is required");
|
||||||
|
return dataSource.updateOne<CustomerUpdateInput, CustomerUpdateOutput>("customers", id, data);
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
export interface Customer {
|
export interface Customer {
|
||||||
id: string;
|
id: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
|
status: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
|
|
||||||
isCompany: string;
|
isCompany: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
tradeName: string;
|
tradeName: string;
|
||||||
tin: string;
|
tin: string;
|
||||||
@ -28,7 +29,7 @@ export interface Customer {
|
|||||||
legalRecord: string;
|
legalRecord: string;
|
||||||
|
|
||||||
defaultTaxes: string[];
|
defaultTaxes: string[];
|
||||||
status: string;
|
|
||||||
languageCode: string;
|
languageCode: string;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,149 @@
|
|||||||
|
import type { QueryClient, QueryKey } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { CustomerToListRowPatchAdapter } from "../adapters";
|
||||||
|
import type { Customer, CustomerList, CustomerListRow } from "../entities";
|
||||||
|
|
||||||
|
import { CUSTOMER_QUERY_KEY, LIST_CUSTOMERS_QUERY_KEY_PREFIX } from "./keys";
|
||||||
|
|
||||||
|
export interface CustomerListCacheSnapshot {
|
||||||
|
key: QueryKey;
|
||||||
|
page?: CustomerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteCustomerCacheContext {
|
||||||
|
snapshots: CustomerListCacheSnapshot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelCustomerListQueries(queryClient: QueryClient) {
|
||||||
|
return queryClient.cancelQueries({
|
||||||
|
queryKey: LIST_CUSTOMERS_QUERY_KEY_PREFIX,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invalidateCustomerListQueries(queryClient: QueryClient) {
|
||||||
|
return queryClient.invalidateQueries({
|
||||||
|
queryKey: LIST_CUSTOMERS_QUERY_KEY_PREFIX,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invalidateCustomerDetailQuery(queryClient: QueryClient, customerId: string) {
|
||||||
|
return queryClient.invalidateQueries({
|
||||||
|
queryKey: CUSTOMER_QUERY_KEY(customerId),
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCustomerDetailCache(
|
||||||
|
queryClient: QueryClient,
|
||||||
|
customerId: string,
|
||||||
|
customer: Customer
|
||||||
|
) {
|
||||||
|
queryClient.setQueryData<Customer>(CUSTOMER_QUERY_KEY(customerId), customer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeCustomerDetailCache(queryClient: QueryClient, customerId: string) {
|
||||||
|
queryClient.removeQueries({
|
||||||
|
queryKey: CUSTOMER_QUERY_KEY(customerId),
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllCustomerListQueryKeys(queryClient: QueryClient): QueryKey[] {
|
||||||
|
const entries = queryClient.getQueriesData<CustomerList>({
|
||||||
|
queryKey: LIST_CUSTOMERS_QUERY_KEY_PREFIX,
|
||||||
|
});
|
||||||
|
|
||||||
|
return entries.map(([key]) => key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function upsertCustomerInListCaches(
|
||||||
|
queryClient: QueryClient,
|
||||||
|
customer: Pick<CustomerListRow, "id"> & Partial<CustomerListRow>
|
||||||
|
) {
|
||||||
|
const keys = getAllCustomerListQueryKeys(queryClient);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const page = queryClient.getQueryData<CustomerList>(key);
|
||||||
|
if (!page) continue;
|
||||||
|
|
||||||
|
const index = page.items.findIndex((row) => row.id === customer.id);
|
||||||
|
if (index === -1) continue;
|
||||||
|
|
||||||
|
const nextItems = page.items.slice();
|
||||||
|
nextItems[index] = {
|
||||||
|
...page.items[index],
|
||||||
|
...customer,
|
||||||
|
};
|
||||||
|
|
||||||
|
queryClient.setQueryData<CustomerList>(key, {
|
||||||
|
...page,
|
||||||
|
items: nextItems,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeCustomerFromListCaches(
|
||||||
|
queryClient: QueryClient,
|
||||||
|
customerId: string
|
||||||
|
): CustomerListCacheSnapshot[] {
|
||||||
|
const snapshots = getAllCustomerListQueryKeys(queryClient).map((key) => ({
|
||||||
|
key,
|
||||||
|
page: queryClient.getQueryData<CustomerList>(key),
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const { key, page } of snapshots) {
|
||||||
|
if (!page) continue;
|
||||||
|
|
||||||
|
queryClient.setQueryData<CustomerList>(key, {
|
||||||
|
...page,
|
||||||
|
items: page.items.filter((row) => row.id !== customerId),
|
||||||
|
total_items: Math.max(0, page.total_items - 1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshots;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreCustomerListCaches(
|
||||||
|
queryClient: QueryClient,
|
||||||
|
snapshots: CustomerListCacheSnapshot[]
|
||||||
|
) {
|
||||||
|
for (const snapshot of snapshots) {
|
||||||
|
queryClient.setQueryData(snapshot.key, snapshot.page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncCreatedCustomerCaches(queryClient: QueryClient, customer: Customer) {
|
||||||
|
setCustomerDetailCache(queryClient, customer.id, customer);
|
||||||
|
return invalidateCustomerListQueries(queryClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncUpdatedCustomerCaches(queryClient: QueryClient, customer: Customer) {
|
||||||
|
setCustomerDetailCache(queryClient, customer.id, customer);
|
||||||
|
upsertCustomerInListCaches(queryClient, CustomerToListRowPatchAdapter.fromCustomer(customer));
|
||||||
|
|
||||||
|
return invalidateCustomerListQueries(queryClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepareDeleteCustomerOptimisticUpdate(
|
||||||
|
queryClient: QueryClient,
|
||||||
|
customerId: string
|
||||||
|
): Promise<DeleteCustomerCacheContext> {
|
||||||
|
await cancelCustomerListQueries(queryClient);
|
||||||
|
const snapshots = removeCustomerFromListCaches(queryClient, customerId);
|
||||||
|
|
||||||
|
return { snapshots };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rollbackDeleteCustomerOptimisticUpdate(
|
||||||
|
queryClient: QueryClient,
|
||||||
|
context?: DeleteCustomerCacheContext
|
||||||
|
) {
|
||||||
|
if (!context) return;
|
||||||
|
restoreCustomerListCaches(queryClient, context.snapshots);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function finalizeDeletedCustomerCaches(queryClient: QueryClient, customerId: string) {
|
||||||
|
removeCustomerDetailCache(queryClient, customerId);
|
||||||
|
return invalidateCustomerListQueries(queryClient);
|
||||||
|
}
|
||||||
30
modules/customers/src/web/shared/hooks/keys.ts
Normal file
30
modules/customers/src/web/shared/hooks/keys.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import type { CriteriaDTO } from "@erp/core";
|
||||||
|
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
|
||||||
|
import type { QueryKey } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export const LIST_CUSTOMERS_QUERY_KEY_PREFIX = ["customers"] as const;
|
||||||
|
export const LIST_CUSTOMERS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey =>
|
||||||
|
[
|
||||||
|
...LIST_CUSTOMERS_QUERY_KEY_PREFIX,
|
||||||
|
{
|
||||||
|
pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX,
|
||||||
|
pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE,
|
||||||
|
q: criteria?.q ?? "",
|
||||||
|
filters: criteria?.filters ?? [],
|
||||||
|
orderBy: criteria?.orderBy ?? "",
|
||||||
|
order: criteria?.order ?? "",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const CUSTOMER_QUERY_KEY = (customerId?: string): QueryKey => [
|
||||||
|
"customers:detail",
|
||||||
|
{
|
||||||
|
customerId,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CUSTOMER_CREATE_KEY = ["customers", "create"] as const;
|
||||||
|
|
||||||
|
export const CUSTOMER_UPDATE_KEY = ["customers", "update"] as const;
|
||||||
|
|
||||||
|
export const CUSTOMER_DELETE_KEY = ["customers", "delete"] as const;
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
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 { GetCustomerByIdAdapter } from "../adapters";
|
||||||
|
import { type CustomerCreateInput, createCustomer } from "../api";
|
||||||
|
import type { Customer } from "../entities";
|
||||||
|
|
||||||
|
import {
|
||||||
|
invalidateCustomerListQueries,
|
||||||
|
syncCreatedCustomerCaches,
|
||||||
|
} from "./customer-cache-strategy";
|
||||||
|
import { CUSTOMER_CREATE_KEY } from "./keys";
|
||||||
|
import { toValidationErrors } from "./to-validation-errors";
|
||||||
|
|
||||||
|
type CreateCustomerContext = {};
|
||||||
|
|
||||||
|
type CreateCustomerPayload = {
|
||||||
|
data: CustomerCreateInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCustomerCreateMutation = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
const schema = CreateCustomerRequestSchema;
|
||||||
|
|
||||||
|
return useMutation<Customer, DefaultError, CreateCustomerPayload, CreateCustomerContext>({
|
||||||
|
mutationKey: CUSTOMER_CREATE_KEY,
|
||||||
|
|
||||||
|
mutationFn: async (payload) => {
|
||||||
|
const { data } = payload;
|
||||||
|
const id = UniqueID.generateNewID().toString();
|
||||||
|
|
||||||
|
const result = schema.safeParse(data);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
const dto = await createCustomer(dataSource, id, data);
|
||||||
|
return GetCustomerByIdAdapter.fromDTO(dto);
|
||||||
|
},
|
||||||
|
|
||||||
|
onSuccess: (createdCustomer) => {
|
||||||
|
syncCreatedCustomerCaches(queryClient, createdCustomer);
|
||||||
|
},
|
||||||
|
onSettled: async () => {
|
||||||
|
await invalidateCustomerListQueries(queryClient);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { deleteCustomerById } from "../api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type DeleteCustomerCacheContext,
|
||||||
|
finalizeDeletedCustomerCaches,
|
||||||
|
invalidateCustomerListQueries,
|
||||||
|
prepareDeleteCustomerOptimisticUpdate,
|
||||||
|
rollbackDeleteCustomerOptimisticUpdate,
|
||||||
|
} from "./customer-cache-strategy";
|
||||||
|
import { CUSTOMER_DELETE_KEY } from "./keys";
|
||||||
|
|
||||||
|
export interface DeleteCustomerPayload {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteCustomerContext extends DeleteCustomerCacheContext {}
|
||||||
|
|
||||||
|
export const useCustomerDeleteMutation = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
|
||||||
|
return useMutation<{ id: string }, DefaultError, DeleteCustomerPayload, DeleteCustomerContext>({
|
||||||
|
mutationKey: CUSTOMER_DELETE_KEY,
|
||||||
|
mutationFn: async ({ id }) => {
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("customerId is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteCustomerById(dataSource, id, new AbortController().signal);
|
||||||
|
return { id };
|
||||||
|
},
|
||||||
|
onMutate: async ({ id }) => {
|
||||||
|
return prepareDeleteCustomerOptimisticUpdate(queryClient, id);
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (_error, _variables, context) => {
|
||||||
|
rollbackDeleteCustomerOptimisticUpdate(queryClient, context);
|
||||||
|
},
|
||||||
|
|
||||||
|
onSuccess: ({ id }) => {
|
||||||
|
finalizeDeletedCustomerCaches(queryClient, id);
|
||||||
|
},
|
||||||
|
onSettled: async () => {
|
||||||
|
await invalidateCustomerListQueries(queryClient);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,52 +1,31 @@
|
|||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import {
|
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
|
||||||
type DefaultError,
|
|
||||||
type QueryKey,
|
|
||||||
type UseQueryResult,
|
|
||||||
useQuery,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { GetCustomerByIdAdapter } from "../adapters";
|
import { GetCustomerByIdAdapter } from "../adapters";
|
||||||
import { getCustomerById } from "../api";
|
import { getCustomerById } from "../api";
|
||||||
import type { Customer } from "../entities";
|
import type { Customer } from "../entities";
|
||||||
|
|
||||||
export const CUSTOMER_QUERY_KEY = (customerId?: string): QueryKey => [
|
import { CUSTOMER_QUERY_KEY } from "./keys";
|
||||||
"customers:detail",
|
|
||||||
{
|
|
||||||
customerId,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
type CustomerQueryOptions = {
|
type CustomerGetQueryOptions = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCustomerGetQuery = (
|
export const useCustomerGetQuery = (
|
||||||
customerId?: string,
|
customerId?: string,
|
||||||
options?: CustomerQueryOptions
|
options?: CustomerGetQueryOptions
|
||||||
): UseQueryResult<Customer, DefaultError> => {
|
): UseQueryResult<Customer, DefaultError> => {
|
||||||
|
//const queryClient = useQueryClient();
|
||||||
const dataSource = useDataSource();
|
const dataSource = useDataSource();
|
||||||
const enabled = options?.enabled ?? Boolean(customerId);
|
const enabled = options?.enabled ?? Boolean(customerId);
|
||||||
|
|
||||||
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, signal, customerId);
|
const dto = await getCustomerById(dataSource, String(customerId), signal);
|
||||||
return GetCustomerByIdAdapter.fromDTO(dto);
|
return GetCustomerByIdAdapter.fromDTO(dto);
|
||||||
},
|
},
|
||||||
enabled,
|
enabled,
|
||||||
placeholderData: (previousData) => previousData, // Mantener datos previos mientras se carga nueva datos (antiguo `keepPreviousData`)
|
//placeholderData <-- No mostrar datos de una tupla anterior mientras se carga nuevos datos
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*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);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|||||||
@ -3,18 +3,22 @@ import { ValidationErrorCollection } from "@repo/rdx-ddd";
|
|||||||
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { UpdateCustomerByIdRequestSchema } from "../../../common";
|
import { UpdateCustomerByIdRequestSchema } from "../../../common";
|
||||||
import type { Customer } from "../api";
|
import { GetCustomerByIdAdapter } from "../adapters";
|
||||||
import type { CustomerData } from "../types";
|
import { type CustomerUpdateInput, updateCustomerById } from "../api";
|
||||||
|
import type { Customer } from "../entities";
|
||||||
|
|
||||||
import { toValidationErrors } from "./toValidationErrors";
|
import {
|
||||||
|
invalidateCustomerListQueries,
|
||||||
export const CUSTOMER_UPDATE_KEY = ["customers", "update"] as const;
|
syncUpdatedCustomerCaches,
|
||||||
|
} from "./customer-cache-strategy";
|
||||||
|
import { CUSTOMER_UPDATE_KEY } from "./keys";
|
||||||
|
import { toValidationErrors } from "./to-validation-errors";
|
||||||
|
|
||||||
type UpdateCustomerContext = {};
|
type UpdateCustomerContext = {};
|
||||||
|
|
||||||
type UpdateCustomerPayload = {
|
type UpdateCustomerPayload = {
|
||||||
id: string;
|
id: string;
|
||||||
data: Partial<CustomerData>;
|
data: CustomerUpdateInput;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useCustomerUpdateMutation = () => {
|
export const useCustomerUpdateMutation = () => {
|
||||||
@ -36,26 +40,15 @@ export const useCustomerUpdateMutation = () => {
|
|||||||
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
throw new ValidationErrorCollection("Validation failed", toValidationErrors(result.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await dataSource.updateOne("customers", customerId, data);
|
const dto = await updateCustomerById(dataSource, customerId, data);
|
||||||
return updated as Customer;
|
return GetCustomerByIdAdapter.fromDTO(dto);
|
||||||
},
|
},
|
||||||
|
|
||||||
onSuccess: (updated: Customer, variables) => {
|
onSuccess: (updatedCustomer) => {
|
||||||
const { id: customerId } = updated;
|
syncUpdatedCustomerCaches(queryClient, updatedCustomer);
|
||||||
|
|
||||||
// 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: async () => {
|
||||||
onSettled: () => {
|
await invalidateCustomerListQueries(queryClient);
|
||||||
// Refresca todos los listados
|
|
||||||
//invalidateCustomerListCache(queryClient);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,30 +1,12 @@
|
|||||||
import type { CriteriaDTO } from "@erp/core";
|
import type { CriteriaDTO } from "@erp/core";
|
||||||
import { useDataSource } from "@erp/core/hooks";
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
import { INITIAL_PAGE_INDEX, INITIAL_PAGE_SIZE } from "@repo/rdx-criteria";
|
import { type DefaultError, type UseQueryResult, useQuery } from "@tanstack/react-query";
|
||||||
import {
|
|
||||||
type DefaultError,
|
|
||||||
type QueryClient,
|
|
||||||
type QueryKey,
|
|
||||||
type UseQueryResult,
|
|
||||||
useQuery,
|
|
||||||
} from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { ListCustomersAdapter } from "../adapters";
|
import { ListCustomersAdapter } from "../adapters";
|
||||||
import { getListCustomers } from "../api";
|
import { getListCustomers } from "../api";
|
||||||
import type { CustomerList, CustomerListRow } from "../entities";
|
import type { CustomerList } from "../entities";
|
||||||
|
|
||||||
export const LIST_CUSTOMERS_QUERY_KEY_PREFIX = ["customers"] as const;
|
import { LIST_CUSTOMERS_QUERY_KEY } from "./keys";
|
||||||
export const LIST_CUSTOMERS_QUERY_KEY = (criteria?: CriteriaDTO): QueryKey => [
|
|
||||||
LIST_CUSTOMERS_QUERY_KEY_PREFIX,
|
|
||||||
{
|
|
||||||
pageNumber: criteria?.pageNumber ?? INITIAL_PAGE_INDEX,
|
|
||||||
pageSize: criteria?.pageSize ?? INITIAL_PAGE_SIZE,
|
|
||||||
q: criteria?.q ?? "",
|
|
||||||
filters: criteria?.filters ?? [],
|
|
||||||
orderBy: criteria?.orderBy ?? "",
|
|
||||||
order: criteria?.order ?? "",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
type ListCustomersQueryOptions = {
|
type ListCustomersQueryOptions = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@ -41,74 +23,10 @@ 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, signal, criteria);
|
const dto = await getListCustomers(dataSource, criteria as CriteriaDTO, signal);
|
||||||
return ListCustomersAdapter.fromDTO(dto);
|
return ListCustomersAdapter.fromDTO(dto);
|
||||||
},
|
},
|
||||||
enabled,
|
enabled,
|
||||||
placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria
|
placeholderData: (previousData) => previousData, // Mantiene la página anterior durante refetch por cambio de criteria
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function cancelListCustomersQueries(qc: QueryClient) {
|
|
||||||
return qc.cancelQueries({ queryKey: LIST_CUSTOMERS_QUERY_KEY_PREFIX });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function invalidateListCustomersQueries(qc: QueryClient) {
|
|
||||||
return qc.invalidateQueries({ queryKey: LIST_CUSTOMERS_QUERY_KEY_PREFIX });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAllListCustomersQueryKeys(qc: QueryClient): QueryKey[] {
|
|
||||||
const entries = qc.getQueriesData<CustomerList>({
|
|
||||||
queryKey: LIST_CUSTOMERS_QUERY_KEY_PREFIX,
|
|
||||||
});
|
|
||||||
|
|
||||||
return entries.map(([key]) => key);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function upsertCustomerInListCustomersCaches(
|
|
||||||
qc: QueryClient,
|
|
||||||
customer: Pick<CustomerListRow, "id"> & Partial<CustomerListRow>
|
|
||||||
) {
|
|
||||||
const keys = getAllListCustomersQueryKeys(qc);
|
|
||||||
|
|
||||||
for (const key of keys) {
|
|
||||||
const page = qc.getQueryData<CustomerList>(key);
|
|
||||||
if (!page) continue;
|
|
||||||
|
|
||||||
const index = page.items.findIndex((row) => row.id === customer.id);
|
|
||||||
if (index === -1) continue;
|
|
||||||
|
|
||||||
const nextItems = page.items.slice();
|
|
||||||
nextItems[index] = {
|
|
||||||
...page.items[index],
|
|
||||||
...customer,
|
|
||||||
};
|
|
||||||
|
|
||||||
qc.setQueryData<CustomerList>(key, {
|
|
||||||
...page,
|
|
||||||
items: nextItems,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeCustomerFromListCustomersCaches(
|
|
||||||
qc: QueryClient,
|
|
||||||
customerId: string
|
|
||||||
): Array<{ key: QueryKey; page?: CustomerList }> {
|
|
||||||
const snapshots = getAllListCustomersQueryKeys(qc).map((key) => ({
|
|
||||||
key,
|
|
||||||
page: qc.getQueryData<CustomerList>(key),
|
|
||||||
}));
|
|
||||||
|
|
||||||
for (const { key, page } of snapshots) {
|
|
||||||
if (!page) continue;
|
|
||||||
|
|
||||||
qc.setQueryData<CustomerList>(key, {
|
|
||||||
...page,
|
|
||||||
items: page.items.filter((row) => row.id !== customerId),
|
|
||||||
total_items: Math.max(0, page.total_items - 1),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return snapshots;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -4,14 +4,9 @@ import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui
|
|||||||
import { useEffect, useId, useMemo } from "react";
|
import { useEffect, useId, useMemo } from "react";
|
||||||
import { type FieldErrors, FormProvider } from "react-hook-form";
|
import { type FieldErrors, FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
import {
|
|
||||||
type Customer,
|
|
||||||
CustomerFormSchema,
|
|
||||||
defaultCustomerFormData,
|
|
||||||
} from "../../_archived/schemas";
|
|
||||||
import { useTranslation } from "../../i18n";
|
import { useTranslation } from "../../i18n";
|
||||||
import { useCustomerGetQuery, useCustomerUpdateMutation } from "../../shared";
|
import { type Customer, useCustomerGetQuery, useCustomerUpdateMutation } from "../../shared";
|
||||||
import type { CustomerFormData } from "../types";
|
import { type CustomerFormData, CustomerFormSchema, defaultCustomerFormData } from "../types";
|
||||||
|
|
||||||
export interface UseCustomerUpdateControllerOptions {
|
export interface UseCustomerUpdateControllerOptions {
|
||||||
onUpdated?(updated: Customer): void;
|
onUpdated?(updated: Customer): void;
|
||||||
@ -56,7 +51,10 @@ export const useCustomerUpdateController = (
|
|||||||
/** Reiniciar el form al recibir datos */
|
/** Reiniciar el form al recibir datos */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 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.
|
||||||
if (customerData) form.reset(customerData, { keepDirty: false });
|
if (customerData) {
|
||||||
|
console.log("Reseteando form con datos del cliente:", customerData);
|
||||||
|
form.reset(customerData, { keepDirty: false });
|
||||||
|
}
|
||||||
}, [customerData, form]);
|
}, [customerData, form]);
|
||||||
|
|
||||||
/** Handlers */
|
/** Handlers */
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./use-customer-form";
|
|
||||||
export * from "./use-customer-update-mutation";
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import { useHookForm } from "@erp/core/hooks";
|
|
||||||
import { useEffect, useMemo } from "react";
|
|
||||||
|
|
||||||
import type { Customer } from "../api";
|
|
||||||
|
|
||||||
function useCustomerForm(customerData: Customer | undefined, isDisabled: boolean) {
|
|
||||||
const initialValues = useMemo(() => customerData ?? defaultCustomerFormData, [customerData]);
|
|
||||||
|
|
||||||
const form = useHookForm()<CustomerFormData>({
|
|
||||||
resolverSchema: CustomerFormSchema,
|
|
||||||
initialValues,
|
|
||||||
disabled: isDisabled,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (customerData) form.reset(customerData);
|
|
||||||
}, [customerData, form]);
|
|
||||||
|
|
||||||
const resetForm = () => form.reset(customerData ?? defaultCustomerFormData);
|
|
||||||
|
|
||||||
return { form, resetForm };
|
|
||||||
}
|
|
||||||
@ -60,11 +60,11 @@ export type CustomerFormData = z.infer<typeof CustomerFormSchema>;
|
|||||||
export const defaultCustomerFormData: CustomerFormData = {
|
export const defaultCustomerFormData: CustomerFormData = {
|
||||||
reference: "",
|
reference: "",
|
||||||
|
|
||||||
is_company: "true",
|
is_company: "",
|
||||||
name: "",
|
name: "",
|
||||||
trade_name: "",
|
trade_name: "",
|
||||||
tin: "",
|
tin: "",
|
||||||
default_taxes: ["iva_21"],
|
default_taxes: [],
|
||||||
|
|
||||||
street: "",
|
street: "",
|
||||||
street2: "",
|
street2: "",
|
||||||
@ -84,6 +84,6 @@ export const defaultCustomerFormData: CustomerFormData = {
|
|||||||
website: "",
|
website: "",
|
||||||
|
|
||||||
legal_record: "",
|
legal_record: "",
|
||||||
language_code: "es",
|
language_code: "",
|
||||||
currency_code: "EUR",
|
currency_code: "",
|
||||||
};
|
};
|
||||||
|
|||||||
@ -30,7 +30,6 @@ export const CustomerAdditionalConfigFields = ({
|
|||||||
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
||||||
<Field className="lg:col-span-2">
|
<Field className="lg:col-span-2">
|
||||||
<SelectField
|
<SelectField
|
||||||
control={control}
|
|
||||||
description={t("form_fields.language_code.description")}
|
description={t("form_fields.language_code.description")}
|
||||||
items={[...LANGUAGE_OPTIONS]}
|
items={[...LANGUAGE_OPTIONS]}
|
||||||
label={t("form_fields.language_code.label")}
|
label={t("form_fields.language_code.label")}
|
||||||
@ -42,7 +41,6 @@ export const CustomerAdditionalConfigFields = ({
|
|||||||
<Field className="lg:col-span-2">
|
<Field className="lg:col-span-2">
|
||||||
<SelectField
|
<SelectField
|
||||||
className="lg:col-span-2"
|
className="lg:col-span-2"
|
||||||
control={control}
|
|
||||||
description={t("form_fields.currency_code.description")}
|
description={t("form_fields.currency_code.description")}
|
||||||
items={[...CURRENCY_OPTIONS]}
|
items={[...CURRENCY_OPTIONS]}
|
||||||
label={t("form_fields.currency_code.label")}
|
label={t("form_fields.currency_code.label")}
|
||||||
|
|||||||
@ -27,7 +27,6 @@ export const CustomerAddressFields = ({ className, ...props }: CustomerAddressFi
|
|||||||
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
||||||
<TextField
|
<TextField
|
||||||
className="lg:col-span-2"
|
className="lg:col-span-2"
|
||||||
control={control}
|
|
||||||
description={t("form_fields.street.description")}
|
description={t("form_fields.street.description")}
|
||||||
label={t("form_fields.street.label")}
|
label={t("form_fields.street.label")}
|
||||||
name="street"
|
name="street"
|
||||||
@ -35,7 +34,6 @@ export const CustomerAddressFields = ({ className, ...props }: CustomerAddressFi
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="lg:col-span-2"
|
className="lg:col-span-2"
|
||||||
control={control}
|
|
||||||
description={t("form_fields.street2.description")}
|
description={t("form_fields.street2.description")}
|
||||||
label={t("form_fields.street2.label")}
|
label={t("form_fields.street2.label")}
|
||||||
name="street2"
|
name="street2"
|
||||||
@ -44,14 +42,12 @@ export const CustomerAddressFields = ({ className, ...props }: CustomerAddressFi
|
|||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="lg:col-span-2"
|
className="lg:col-span-2"
|
||||||
control={control}
|
|
||||||
description={t("form_fields.city.description")}
|
description={t("form_fields.city.description")}
|
||||||
label={t("form_fields.city.label")}
|
label={t("form_fields.city.label")}
|
||||||
name="city"
|
name="city"
|
||||||
placeholder={t("form_fields.city.placeholder")}
|
placeholder={t("form_fields.city.placeholder")}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
control={control}
|
|
||||||
description={t("form_fields.postal_code.description")}
|
description={t("form_fields.postal_code.description")}
|
||||||
label={t("form_fields.postal_code.label")}
|
label={t("form_fields.postal_code.label")}
|
||||||
name="postal_code"
|
name="postal_code"
|
||||||
@ -60,7 +56,6 @@ export const CustomerAddressFields = ({ className, ...props }: CustomerAddressFi
|
|||||||
|
|
||||||
<Field className="lg:col-span-2 lg:col-start-1">
|
<Field className="lg:col-span-2 lg:col-start-1">
|
||||||
<TextField
|
<TextField
|
||||||
control={control}
|
|
||||||
description={t("form_fields.province.description")}
|
description={t("form_fields.province.description")}
|
||||||
label={t("form_fields.province.label")}
|
label={t("form_fields.province.label")}
|
||||||
name="province"
|
name="province"
|
||||||
@ -69,7 +64,6 @@ export const CustomerAddressFields = ({ className, ...props }: CustomerAddressFi
|
|||||||
</Field>
|
</Field>
|
||||||
<Field className="lg:col-span-2">
|
<Field className="lg:col-span-2">
|
||||||
<SelectField
|
<SelectField
|
||||||
control={control}
|
|
||||||
description={t("form_fields.country.description")}
|
description={t("form_fields.country.description")}
|
||||||
items={[...COUNTRY_OPTIONS]}
|
items={[...COUNTRY_OPTIONS]}
|
||||||
label={t("form_fields.country.label")}
|
label={t("form_fields.country.label")}
|
||||||
|
|||||||
@ -32,7 +32,6 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
|
|||||||
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
||||||
<TextField
|
<TextField
|
||||||
className="lg:col-span-2"
|
className="lg:col-span-2"
|
||||||
control={control}
|
|
||||||
description={t("form_fields.email_primary.description")}
|
description={t("form_fields.email_primary.description")}
|
||||||
label={t("form_fields.email_primary.label")}
|
label={t("form_fields.email_primary.label")}
|
||||||
leftIcon={
|
leftIcon={
|
||||||
@ -46,7 +45,6 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
|
|||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="lg:col-span-2"
|
className="lg:col-span-2"
|
||||||
control={control}
|
|
||||||
description={t("form_fields.mobile_primary.description")}
|
description={t("form_fields.mobile_primary.description")}
|
||||||
label={t("form_fields.mobile_primary.label")}
|
label={t("form_fields.mobile_primary.label")}
|
||||||
leftIcon={
|
leftIcon={
|
||||||
@ -62,7 +60,6 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
|
|||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="lg:col-span-2"
|
className="lg:col-span-2"
|
||||||
control={control}
|
|
||||||
description={t("form_fields.phone_primary.description")}
|
description={t("form_fields.phone_primary.description")}
|
||||||
label={t("form_fields.phone_primary.label")}
|
label={t("form_fields.phone_primary.label")}
|
||||||
leftIcon={
|
leftIcon={
|
||||||
@ -77,7 +74,6 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
|
|||||||
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
<FieldGroup className="grid grid-cols-1 gap-x-6 lg:grid-cols-4">
|
||||||
<TextField
|
<TextField
|
||||||
className="lg:col-span-2 lg:col-start-1"
|
className="lg:col-span-2 lg:col-start-1"
|
||||||
control={control}
|
|
||||||
description={t("form_fields.email_secondary.description")}
|
description={t("form_fields.email_secondary.description")}
|
||||||
label={t("form_fields.email_secondary.label")}
|
label={t("form_fields.email_secondary.label")}
|
||||||
leftIcon={
|
leftIcon={
|
||||||
@ -90,7 +86,6 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
|
|||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
className="lg:col-span-2"
|
className="lg:col-span-2"
|
||||||
control={control}
|
|
||||||
description={t("form_fields.mobile_secondary.description")}
|
description={t("form_fields.mobile_secondary.description")}
|
||||||
label={t("form_fields.mobile_secondary.label")}
|
label={t("form_fields.mobile_secondary.label")}
|
||||||
leftIcon={
|
leftIcon={
|
||||||
@ -105,7 +100,6 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
className="lg:col-span-2"
|
className="lg:col-span-2"
|
||||||
control={control}
|
|
||||||
description={t("form_fields.phone_secondary.description")}
|
description={t("form_fields.phone_secondary.description")}
|
||||||
label={t("form_fields.phone_secondary.label")}
|
label={t("form_fields.phone_secondary.label")}
|
||||||
leftIcon={
|
leftIcon={
|
||||||
@ -132,7 +126,6 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
|
|||||||
<Field className="lg:col-span-2">
|
<Field className="lg:col-span-2">
|
||||||
<TextField
|
<TextField
|
||||||
className="lg:col-span-2"
|
className="lg:col-span-2"
|
||||||
control={control}
|
|
||||||
description={t("form_fields.website.description")}
|
description={t("form_fields.website.description")}
|
||||||
label={t("form_fields.website.label")}
|
label={t("form_fields.website.label")}
|
||||||
leftIcon={
|
leftIcon={
|
||||||
@ -149,7 +142,6 @@ export const CustomerContactFields = ({ className, ...props }: CustomerContactFi
|
|||||||
<Field className="lg:col-span-2">
|
<Field className="lg:col-span-2">
|
||||||
<TextField
|
<TextField
|
||||||
className="lg:col-span-2"
|
className="lg:col-span-2"
|
||||||
control={control}
|
|
||||||
description={t("form_fields.fax.description")}
|
description={t("form_fields.fax.description")}
|
||||||
label={t("form_fields.fax.label")}
|
label={t("form_fields.fax.label")}
|
||||||
name="fax"
|
name="fax"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user