PROFORMAS UPDATE

This commit is contained in:
David Arranz 2026-04-07 21:44:51 +02:00
parent 982ed7d562
commit bd209374bc
49 changed files with 964 additions and 394 deletions

View File

@ -3,4 +3,5 @@ export * from "./proforma-finder.di";
export * from "./proforma-input-mappers.di";
export * from "./proforma-issuer.di";
export * from "./proforma-snapshot-builders.di";
export * from "./proforma-updater.di";
export * from "./proforma-use-cases.di";

View File

@ -1,19 +1,26 @@
import type { ICatalogs } from "@erp/core/api";
import { CreateProformaInputMapper, type ICreateProformaInputMapper } from "../mappers";
import {
CreateProformaInputMapper,
type ICreateProformaInputMapper,
type IUpdateProformaInputMapper,
UpdateProformaInputMapper,
} from "../mappers";
export interface IProformaInputMappers {
createInputMapper: ICreateProformaInputMapper;
updateInputMapper: IUpdateProformaInputMapper;
}
export const buildProformaInputMappers = (catalogs: ICatalogs): IProformaInputMappers => {
const { taxCatalog } = catalogs;
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
// Mappers el DTO a las props validadas (ProformaProps) y luego construir agregado
const createInputMapper = new CreateProformaInputMapper({ taxCatalog });
//const updateProformaInputMapper = new UpdateProformaInputMapper();
const updateInputMapper = new UpdateProformaInputMapper();
return {
createInputMapper,
updateInputMapper,
};
};

View File

@ -0,0 +1,12 @@
import type { IProformaRepository } from "../repositories";
import { type IProformaUpdater, ProformaUpdater } from "../services";
export const buildProformaUpdater = (params: {
repository: IProformaRepository;
}): IProformaUpdater => {
const { repository } = params;
return new ProformaUpdater({
repository,
});
};

View File

@ -1,11 +1,12 @@
import type { ITransactionManager } from "@erp/core/api";
import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
import type { ICreateProformaInputMapper } from "../mappers";
import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers";
import type {
IProformaCreator,
IProformaFinder,
IProformaIssuer,
IProformaUpdater,
ProformaDocumentGeneratorService,
} from "../services";
import type {
@ -20,6 +21,7 @@ import {
ListProformasUseCase,
ReportProformaUseCase,
} from "../use-cases";
import { UpdateProformaUseCase } from "../use-cases/update-proforma.use-case";
export function buildGetProformaByIdUseCase(deps: {
finder: IProformaFinder;
@ -93,18 +95,25 @@ export function buildIssueProformaUseCase(deps: {
});
}
/*export function buildUpdateProformaUseCase(deps: {
finder: IProformaFinder;
export function buildUpdateProformaUseCase(deps: {
updater: IProformaUpdater;
dtoMapper: IUpdateProformaInputMapper;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new UpdateProformaUseCase(deps.finder, deps.fullSnapshotBuilder);
return new UpdateProformaUseCase({
dtoMapper: deps.dtoMapper,
updater: deps.updater,
fullSnapshotBuilder: deps.fullSnapshotBuilder,
transactionManager: deps.transactionManager,
});
}
/*
export function buildDeleteProformaUseCase(deps: { finder: IProformaFinder }) {
return new DeleteProformaUseCase(deps.finder);
}
export function buildChangeStatusProformaUseCase(deps: {
finder: IProformaFinder;
transactionManager: ITransactionManager;

View File

@ -17,7 +17,7 @@ import { InvoiceSerie, type ProformaPatchProps } from "../../../domain";
/**
* UpdateProformaPropsMapper
* Convierte el DTO a las props validadas (CustomerInvoiceProps).
* Convierte el DTO a las props validadas (ProformaInvoiceProps).
* No construye directamente el agregado.
* Tri-estado:
* - campo omitido no se cambia
@ -29,105 +29,114 @@ import { InvoiceSerie, type ProformaPatchProps } from "../../../domain";
*
*/
export function UpdateProformaInputMapper(dto: UpdateProformaByIdRequestDTO) {
try {
const errors: ValidationErrorDetail[] = [];
const props: ProformaPatchProps = {};
export interface IUpdateProformaInputMapper {
map(
dto: UpdateProformaByIdRequestDTO,
params: { companyId: UniqueID }
): Result<ProformaPatchProps>;
}
toPatchField(dto.series).ifSet((series) => {
props.series = extractOrPushError(
maybeFromNullableResult(series, (value) => InvoiceSerie.create(value)),
"reference",
errors
);
});
export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
public map(dto: UpdateProformaByIdRequestDTO, params: { companyId: UniqueID }) {
try {
const errors: ValidationErrorDetail[] = [];
const props: ProformaPatchProps = {};
toPatchField(dto.invoice_date).ifSet((invoice_date) => {
if (isNullishOrEmpty(invoice_date)) {
errors.push({ path: "invoice_date", message: "Invoice date cannot be empty" });
return;
}
props.invoiceDate = extractOrPushError(
UtcDate.createFromISO(invoice_date!),
"invoice_date",
errors
);
});
toPatchField(dto.series).ifSet((series) => {
props.series = extractOrPushError(
maybeFromNullableResult(series, (value) => InvoiceSerie.create(value)),
"reference",
errors
);
});
toPatchField(dto.operation_date).ifSet((operation_date) => {
props.operationDate = extractOrPushError(
maybeFromNullableResult(operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
});
toPatchField(dto.invoice_date).ifSet((invoice_date) => {
if (isNullishOrEmpty(invoice_date)) {
errors.push({ path: "invoice_date", message: "Invoice date cannot be empty" });
return;
}
props.invoiceDate = extractOrPushError(
UtcDate.createFromISO(invoice_date!),
"invoice_date",
errors
);
});
toPatchField(dto.customer_id).ifSet((customer_id) => {
if (isNullishOrEmpty(customer_id)) {
errors.push({ path: "customer_id", message: "Customer cannot be empty" });
return;
}
props.customerId = extractOrPushError(UniqueID.create(customer_id!), "customer_id", errors);
});
toPatchField(dto.operation_date).ifSet((operation_date) => {
props.operationDate = extractOrPushError(
maybeFromNullableResult(operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
});
toPatchField(dto.reference).ifSet((reference) => {
props.reference = extractOrPushError(
maybeFromNullableResult(reference, (value) => Result.ok(String(value))),
"reference",
errors
);
});
toPatchField(dto.customer_id).ifSet((customer_id) => {
if (isNullishOrEmpty(customer_id)) {
errors.push({ path: "customer_id", message: "Proforma cannot be empty" });
return;
}
props.customerId = extractOrPushError(UniqueID.create(customer_id!), "customer_id", errors);
});
toPatchField(dto.description).ifSet((description) => {
props.description = extractOrPushError(
maybeFromNullableResult(description, (value) => Result.ok(String(value))),
"description",
errors
);
});
toPatchField(dto.reference).ifSet((reference) => {
props.reference = extractOrPushError(
maybeFromNullableResult(reference, (value) => Result.ok(String(value))),
"reference",
errors
);
});
toPatchField(dto.notes).ifSet((notes) => {
props.notes = extractOrPushError(
maybeFromNullableResult(notes, (value) => TextValue.create(value)),
"notes",
errors
);
});
toPatchField(dto.description).ifSet((description) => {
props.description = extractOrPushError(
maybeFromNullableResult(description, (value) => Result.ok(String(value))),
"description",
errors
);
});
toPatchField(dto.language_code).ifSet((languageCode) => {
if (isNullishOrEmpty(languageCode)) {
errors.push({ path: "language_code", message: "Language code cannot be empty" });
return;
toPatchField(dto.notes).ifSet((notes) => {
props.notes = extractOrPushError(
maybeFromNullableResult(notes, (value) => TextValue.create(value)),
"notes",
errors
);
});
toPatchField(dto.language_code).ifSet((languageCode) => {
if (isNullishOrEmpty(languageCode)) {
errors.push({ path: "language_code", message: "Language code cannot be empty" });
return;
}
props.languageCode = extractOrPushError(
LanguageCode.create(languageCode!),
"language_code",
errors
);
});
toPatchField(dto.currency_code).ifSet((currencyCode) => {
if (isNullishOrEmpty(currencyCode)) {
errors.push({ path: "currency_code", message: "Currency code cannot be empty" });
return;
}
props.currencyCode = extractOrPushError(
CurrencyCode.create(currencyCode!),
"currency_code",
errors
);
});
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Proforma invoice props mapping failed (update)", errors)
);
}
props.languageCode = extractOrPushError(
LanguageCode.create(languageCode!),
"language_code",
errors
);
});
toPatchField(dto.currency_code).ifSet((currencyCode) => {
if (isNullishOrEmpty(currencyCode)) {
errors.push({ path: "currency_code", message: "Currency code cannot be empty" });
return;
}
props.currencyCode = extractOrPushError(
CurrencyCode.create(currencyCode!),
"currency_code",
errors
);
});
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice props mapping failed (update)", errors)
);
return Result.ok(props);
} catch (err: unknown) {
return Result.fail(new DomainError("Proforma invoice props mapping failed", { cause: err }));
}
return Result.ok(props);
} catch (err: unknown) {
return Result.fail(new DomainError("Customer invoice props mapping failed", { cause: err }));
}
}

View File

@ -6,3 +6,4 @@ export * from "./proforma-finder";
export * from "./proforma-issuer";
export * from "./proforma-number-generator.interface";
export * from "./proforma-public-services.interface";
export * from "./proforma-updater";

View File

@ -0,0 +1,64 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Proforma, ProformaPatchProps } from "../../../domain";
import type { IProformaRepository } from "../repositories";
export interface IProformaUpdater {
update(params: {
companyId: UniqueID;
id: UniqueID;
props: ProformaPatchProps;
transaction: unknown;
}): Promise<Result<Proforma, Error>>;
}
type ProformaUpdaterDeps = {
repository: IProformaRepository;
};
export class ProformaUpdater implements IProformaUpdater {
private readonly repository: IProformaRepository;
constructor(deps: ProformaUpdaterDeps) {
this.repository = deps.repository;
}
async update(params: {
companyId: UniqueID;
id: UniqueID;
props: ProformaPatchProps;
transaction: unknown;
}): Promise<Result<Proforma, Error>> {
const { companyId, id, props, transaction } = params;
console.log("props => ", props);
// Recuperar agregado existente
const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
if (existingResult.isFailure) {
return Result.fail(existingResult.error);
}
const proforma = existingResult.data;
// Aplicar cambios en el agregado
const updateResult = proforma.update(props);
if (updateResult.isFailure) {
return Result.fail(updateResult.error);
}
console.log(proforma.operationDate);
// Persistir cambios
const saveResult = await this.repository.update(proforma, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
return Result.ok(proforma);
}
}

View File

@ -5,4 +5,4 @@ export * from "./get-proforma-by-id.use-case";
export * from "./issue-proforma.use-case";
export * from "./list-proformas.use-case";
export * from "./report-proforma.use-case";
//export * from "./update-proforma";
export * from "./update-proforma.use-case";

View File

@ -48,7 +48,7 @@ export class IssueProformaUseCase {
return this.transactionManager.complete(async (transaction) => {
try {
// 1. Recuperamos la issuedinvoice
// 1. Recuperamos la proforma
const proformaResult = await this.finder.findProformaById(
companyId,
proformaId,

View File

@ -0,0 +1,124 @@
import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { UpdateProformaByIdRequestDTO } from "../../../../common";
import type { ProformaPatchProps } from "../../../domain";
import type { IUpdateProformaInputMapper } from "../mappers";
import type { IProformaUpdater } from "../services";
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
type UpdateProformaUseCaseInput = {
companyId: UniqueID;
proforma_id: string;
dto: UpdateProformaByIdRequestDTO;
};
type UpdateProformaUseCaseDeps = {
dtoMapper: IUpdateProformaInputMapper;
updater: IProformaUpdater;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager;
};
export class UpdateProformaUseCase {
private readonly dtoMapper: IUpdateProformaInputMapper;
private readonly updater: IProformaUpdater;
private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder;
private readonly transactionManager: ITransactionManager;
constructor(deps: UpdateProformaUseCaseDeps) {
this.dtoMapper = deps.dtoMapper;
this.updater = deps.updater;
this.fullSnapshotBuilder = deps.fullSnapshotBuilder;
this.transactionManager = deps.transactionManager;
}
public execute(params: UpdateProformaUseCaseInput) {
const { companyId, proforma_id, dto } = params;
const proformaIdOrError = UniqueID.create(proforma_id);
if (proformaIdOrError.isFailure) {
return Result.fail(proformaIdOrError.error);
}
const proformaId = proformaIdOrError.data;
// Mapear DTO → props de dominio
const patchPropsResult = this.dtoMapper.map(dto, { companyId });
if (patchPropsResult.isFailure) {
return patchPropsResult;
}
const patchProps: ProformaPatchProps = patchPropsResult.data;
console.log("dto => ", dto);
console.log("patchProps => ", patchProps);
return this.transactionManager.complete(async (transaction) => {
try {
const updateResult = await this.updater.update({
companyId,
id: proformaId,
props: patchProps,
transaction,
});
if (updateResult.isFailure) {
return Result.fail(updateResult.error);
}
return Result.ok(this.fullSnapshotBuilder.toOutput(updateResult.data));
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}
/*
const presenter = this.presenterRegistry.getPresenter({
resource: "proforma",
projection: "FULL",
}) as ProformaFullPresenter;
// Mapear DTO → props de dominio
const patchPropsResult = mapDTOToUpdateProformaInvoicePatchProps(dto);
if (patchPropsResult.isFailure) {
return Result.fail(patchPropsResult.error);
}
const patchProps: ProformaPatchProps = patchPropsResult.data;
return this.transactionManager.complete(async (transaction: unknown) => {
try {
const updatedInvoice = await this.service.patchProformaByIdInCompany(
companyId,
invoiceId,
patchProps,
transaction
);
if (updatedInvoice.isFailure) {
return Result.fail(updatedInvoice.error);
}
const invoiceOrError = await this.service.updateProformaInCompany(
companyId,
updatedInvoice.data,
transaction
);
if (invoiceOrError.isFailure) return Result.fail(invoiceOrError.error);
const invoice = invoiceOrError.data;
const dto = presenter.toOutput(invoice);
return Result.ok(dto);
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}
*/

View File

@ -1 +0,0 @@
export * from "./update-proforma.use-case";

View File

@ -1,73 +0,0 @@
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { UpdateProformaByIdRequestDTO } from "../../../../../common";
import type { ProformaPatchProps } from "../../../../domain";
import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service";
import type { ProformaFullPresenter } from "../../../snapshot-builders";
type UpdateProformaUseCaseInput = {
companyId: UniqueID;
proforma_id: string;
dto: UpdateProformaByIdRequestDTO;
};
export class UpdateProformaUseCase {
constructor(
private readonly service: CustomerInvoiceApplicationService,
private readonly transactionManager: ITransactionManager,
private readonly presenterRegistry: IPresenterRegistry
) {}
public execute(params: UpdateProformaUseCaseInput) {
const { companyId, proforma_id, dto } = params;
const idOrError = UniqueID.create(proforma_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const invoiceId = idOrError.data;
const presenter = this.presenterRegistry.getPresenter({
resource: "proforma",
projection: "FULL",
}) as ProformaFullPresenter;
// Mapear DTO → props de dominio
const patchPropsResult = mapDTOToUpdateCustomerInvoicePatchProps(dto);
if (patchPropsResult.isFailure) {
return Result.fail(patchPropsResult.error);
}
const patchProps: ProformaPatchProps = patchPropsResult.data;
return this.transactionManager.complete(async (transaction: unknown) => {
try {
const updatedInvoice = await this.service.patchProformaByIdInCompany(
companyId,
invoiceId,
patchProps,
transaction
);
if (updatedInvoice.isFailure) {
return Result.fail(updatedInvoice.error);
}
const invoiceOrError = await this.service.updateProformaInCompany(
companyId,
updatedInvoice.data,
transaction
);
if (invoiceOrError.isFailure) return Result.fail(invoiceOrError.error);
const invoice = invoiceOrError.data;
const dto = presenter.toOutput(invoice);
return Result.ok(dto);
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -7,6 +7,7 @@ import {
type IssueProformaUseCase,
type ListProformasUseCase,
type ReportProformaUseCase,
type UpdateProformaUseCase,
buildCreateProformaUseCase,
buildGetProformaByIdUseCase,
buildIssueProformaUseCase,
@ -17,7 +18,9 @@ import {
buildProformaIssuer,
buildProformaSnapshotBuilders,
buildProformaToIssuedInvoicePropsConverter,
buildProformaUpdater,
buildReportProformaUseCase,
buildUpdateProformaUseCase,
} from "../../../application";
import { buildProformaDocumentService } from "./proforma-documents.di";
@ -34,9 +37,9 @@ export type ProformasInternalDeps = {
issueProforma: (publicServices: {
issuedInvoiceServices: IIssuedInvoicePublicServices;
}) => IssueProformaUseCase;
updateProforma: () => UpdateProformaUseCase;
/*
updateProforma: () => UpdateProformaUseCase;
deleteProforma: () => DeleteProformaUseCase;
issueProforma: () => IssueProformaUseCase;
changeStatusProforma: () => ChangeStatusProformaUseCase;*/
@ -65,6 +68,8 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
repository,
});
const updater = buildProformaUpdater({ repository });
const snapshotBuilders = buildProformaSnapshotBuilders();
const documentGeneratorPipeline = buildProformaDocumentService(params);
@ -102,6 +107,14 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
transactionManager,
}),
updateProforma: () =>
buildUpdateProformaUseCase({
updater,
dtoMapper: inputMappers.updateInputMapper,
fullSnapshotBuilder: snapshotBuilders.full,
transactionManager,
}),
issueProforma: (publicServices: { issuedInvoiceServices: IIssuedInvoicePublicServices }) =>
buildIssueProformaUseCase({
publicServices,

View File

@ -17,10 +17,13 @@ import {
ListProformasRequestSchema,
ReportProformaByIdParamsRequestSchema,
ReportProformaByIdQueryRequestSchema,
UpdateProformaByIdParamsRequestSchema,
UpdateProformaByIdRequestSchema,
} from "../../../../common";
import type { IIssuedInvoicePublicServices } from "../../../application";
import { CreateProformaController } from "./controllers/create-proforma.controller";
import { UpdateProformaController } from "./controllers/update-proforma.controller";
export const proformasRouter = (params: StartParams) => {
const { app, config, getService, getInternal } = params;
@ -102,7 +105,6 @@ export const proformasRouter = (params: StartParams) => {
}
);
/*
router.put(
"/:proforma_id",
//checkTabContext,
@ -110,13 +112,13 @@ export const proformasRouter = (params: StartParams) => {
validateRequest(UpdateProformaByIdParamsRequestSchema, "params"),
validateRequest(UpdateProformaByIdRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.update_proforma();
const useCase = deps.useCases.updateProforma();
const controller = new UpdateProformaController(useCase);
return controller.execute(req, res, next);
}
);
router.delete(
/*router.delete(
"/:proforma_id",
//checkTabContext,

View File

@ -14,9 +14,9 @@ const ProformasListPage = lazy(() =>
import("./proformas/create").then((m) => ({ default: m.ProformaCreatePage }))
);*/
/*const InvoiceUpdatePage = lazy(() =>
import("./pages").then((m) => ({ default: m.InvoiceUpdatePage }))
);*/
const ProformaUpdatePage = lazy(() =>
import("./proformas/update").then((m) => ({ default: m.ProformaUpdatePage }))
);
const IssuedInvoicesLayout = lazy(() =>
import("./issued-invoices/ui").then((m) => ({ default: m.IssuedInvoicesLayout }))
@ -39,7 +39,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
{ path: "", index: true, element: <ProformasListPage /> }, // index
{ path: "list", element: <ProformasListPage /> },
//{ path: "create", element: <ProformaCreatePage /> },
//{ path: ":id/edit", element: <InvoiceUpdatePage /> },
{ path: ":id/edit", element: <ProformaUpdatePage /> },
],
},
{
@ -55,7 +55,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
/*
{ path: "create", element: <CustomerInvoicesList /> },
{ path: ":id", element: <CustomerInvoicesList /> },
{ path: ":id/edit", element: <CustomerInvoicesList /> },
{ path: ":id/delete", element: <CustomerInvoicesList /> },
{ path: ":id/view", element: <CustomerInvoicesList /> },
{ path: ":id/print", element: <CustomerInvoicesList /> },

View File

@ -2,4 +2,3 @@ export * from "../proformas";
export * from "./create";
export * from "./list";
export * from "./update";

View File

@ -1,3 +1,3 @@
export * from "./proforma-create-form.entity";
export * from "./proforma-create-form.schema";
export * from "./proforma-create-form-default";
export * from "./proforma-create-form-default.entity";

View File

@ -0,0 +1 @@
export * from "./proforma-to-proforma-update-form.adapter";

View File

@ -0,0 +1,31 @@
import type { Proforma } from "../../shared";
import type { ProformaUpdateForm } from "../entities";
/**
* Mapea un cliente a un formulario de actualización de cliente.
*
* @param proforma
* @returns
*/
export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpdateForm => {
return {
series: proforma.series ?? "",
invoiceDate: proforma.invoiceDate ?? "",
operationDate: proforma.operationDate ?? "",
customerId: proforma.customerId ?? "",
description: proforma.description ?? "",
reference: proforma.reference ?? "",
notes: proforma.notes ?? "",
languageCode: proforma.languageCode ?? "es",
currencyCode: proforma.currencyCode ?? "EUR",
globalDiscountPercentage: proforma.globalDiscountPercentage ?? 0,
paymentMethod: proforma.paymentMethod ?? "",
};
};

View File

@ -1 +1,2 @@
export * from "./use-proforma-update-page.controller";
export * from "./use-update-proforma-controller";
export * from "./use-update-proforma-page-controller";

View File

@ -0,0 +1,171 @@
import { useHookForm } from "@erp/core/hooks";
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
import { useEffect, useId, useMemo } from "react";
import type { FieldErrors } from "react-hook-form";
import { useTranslation } from "../../../i18n";
import type { UpdateProformaByIdParams } from "../../shared";
import type { Proforma } from "../../shared/entities";
import { useProformaGetQuery, useProformaUpdateMutation } from "../../shared/hooks";
import { mapProformaToProformaUpdateForm } from "../adapters";
import {
type ProformaUpdateForm,
ProformaUpdateFormSchema,
defaultProformaUpdateForm,
} from "../entities";
import {
buildProformaUpdatePatch,
buildUpdateProformaByIdParams,
focusFirstProformaUpdateError,
} from "../utils";
export interface UseUpdateProformaControllerOptions {
onUpdated?(updated: Proforma): void;
successToasts?: boolean;
onError?(error: Error, params: UpdateProformaByIdParams): void;
errorToasts?: boolean;
}
export const useUpdateProformaController = (
proformaId?: string,
options?: UseUpdateProformaControllerOptions
) => {
const { t } = useTranslation();
const formId = useId();
// 1) Estado de carga de la proforma (query)
const {
data: proformaData,
isLoading,
isError: isLoadError,
error: loadError,
} = useProformaGetQuery({ id: proformaId, enabled: Boolean(proformaId) });
// 2) Estado de creación (mutación)
const {
mutateAsync,
isPending: isUpdating,
isError: isUpdateError,
error: updateError,
} = useProformaUpdateMutation();
const initialValues = useMemo<ProformaUpdateForm>(() => {
if (!proformaData) return defaultProformaUpdateForm;
return mapProformaToProformaUpdateForm(proformaData);
}, [proformaData]);
// 3) Form hook
const form = useHookForm<ProformaUpdateForm>({
resolverSchema: ProformaUpdateFormSchema,
initialValues,
disabled: isLoading || isUpdating,
});
useEffect(() => {
if (!proformaData) return;
console.log("Reseteando form con datos de la proforma:", proformaData);
form.reset(mapProformaToProformaUpdateForm(proformaData), {
keepDirty: false, // <-- importante: no marca el form como "dirty" al cargar los datos reales
});
}, [proformaData, form]);
/** Handlers */
const resetForm = () => {
const initialData = proformaData
? mapProformaToProformaUpdateForm(proformaData)
: defaultProformaUpdateForm;
form.reset(initialData, { keepDirty: false });
};
const submitHandler = form.handleSubmit(
async (formData: ProformaUpdateForm) => {
if (!proformaId) {
showErrorToast(t("proformas.update.error.title"), t("proformas.update.error.missing_id"));
return;
}
const previousData = proformaData;
const patchData = buildProformaUpdatePatch(formData, form.formState.dirtyFields);
const params = buildUpdateProformaByIdParams(proformaId, patchData);
try {
// Enviamos cambios al servidor
const updated = await mutateAsync(params);
// Ha ido bien -> actualizamos form con datos reales
// keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render.
form.reset(mapProformaToProformaUpdateForm(updated), {
keepDirty: false,
});
if (options?.successToasts !== false) {
showSuccessToast(
t("proformas.update.success.title"),
t("proformas.update.success.message")
);
}
options?.onUpdated?.(updated);
} catch (error: unknown) {
const normalizedError =
error instanceof Error ? error : new Error(t("pages.update.error.unknown"));
form.reset(
previousData ? mapProformaToProformaUpdateForm(previousData) : defaultProformaUpdateForm,
{ keepDirty: false }
);
if (options?.errorToasts !== false) {
showErrorToast(t("proformas.update.error.title"), normalizedError.message);
}
options?.onError?.(normalizedError, params);
}
},
(errors: FieldErrors<ProformaUpdateForm>) => {
focusFirstProformaUpdateError(errors);
showWarningToast(
t("proformas.update.validation.title"),
t("proformas.update.validation.message")
);
}
);
// 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
formId,
form,
// handlers del form
onSubmit,
resetForm,
// carga de datos
proformaData,
isLoading,
isLoadError,
loadError,
// mutation
isUpdating,
isUpdateError,
updateError,
// No devolver FormProvider, así el controller es más
// flexible y reusable (p.ej. para un modal)
// FormProvider,
};
};

View File

@ -0,0 +1,13 @@
import { useUrlParamId } from "@erp/core/hooks";
import { useUpdateProformaController } from "./use-update-proforma-controller";
export const useUpdateProformaPageController = () => {
const proformaId = useUrlParamId();
const updateCtrl = useUpdateProformaController(proformaId);
return {
updateCtrl,
};
};

View File

@ -1,4 +1,5 @@
export * from "./proforma-item-update-form.entity";
export * from "./proforma-update-form.entity";
export * from "./proforma-update-form.schema";
export * from "./proforma-update-form-defaults";
export * from "./proforma-update-form-default.entity";
export * from "./proforma-update-patch.entity";

View File

@ -0,0 +1,21 @@
import type { ProformaUpdateForm } from ".";
export const defaultProformaUpdateForm: ProformaUpdateForm = {
series: "",
invoiceDate: "",
operationDate: "",
customerId: "",
description: "",
reference: "",
notes: "",
languageCode: "es",
currencyCode: "EUR",
paymentMethod: "",
globalDiscountPercentage: 0,
};

View File

@ -1,33 +0,0 @@
import type { CustomerUpdateForm } from "./proforma-update-form.entity";
export const defaultCustomerUpdateForm: CustomerUpdateForm = {
reference: "",
isCompany: true,
name: "",
tradeName: "",
tin: "",
defaultTaxes: [],
street: "",
street2: "",
city: "",
province: "",
postalCode: "",
country: "es",
primaryEmail: "",
secondaryEmail: "",
primaryPhone: "",
secondaryPhone: "",
primaryMobile: "",
secondaryMobile: "",
fax: "",
website: "",
legalRecord: "",
languageCode: "es",
currencyCode: "EUR",
};

View File

@ -13,8 +13,6 @@
* - sin detalles impuestos por el widget
*/
import type { ProformaItemForm } from "../../shared/entities";
export interface ProformaUpdateForm {
series: string;
@ -23,6 +21,7 @@ export interface ProformaUpdateForm {
customerId: string;
description: string;
reference: string;
notes: string;
@ -33,5 +32,5 @@ export interface ProformaUpdateForm {
paymentMethod: string;
items: ProformaItemForm[];
//items: ProformaItemForm[];
}

View File

@ -14,7 +14,29 @@ import { z } from "zod/v4";
* - sin detalles impuestos por el widget
*/
export const ProformaItemFormSchema = z.object({
export const ProformaUpdateFormSchema = z.object({
series: z.string().default(""),
invoiceDate: z.string().default(""),
operationDate: z.string().default(""),
customerId: z.string().default(""),
description: z.string().default(""),
reference: z.string().default(""),
notes: z.string().default(""),
languageCode: z.string().min(1, "Debe indicar un idioma").default("es"),
currencyCode: z.string().min(1, "Debe indicar una moneda").default("EUR"),
globalDiscountPercentage: z.number().default(0),
paymentMethod: z.string().default(""),
});
export type ProformaUpdateFormSchemaType = z.infer<typeof ProformaUpdateFormSchema>;
const ProformaItemFormSchema = z.object({
description: z.string().max(2000).optional().default(""),
quantity: z.any(), //NumericStringSchema.optional(),
unit_amount: z.any(), //NumericStringSchema.optional(),
@ -30,7 +52,7 @@ export const ProformaItemFormSchema = z.object({
total_amount: z.number(),
});
export const ProformaFormSchema = z.object({
const ProformaFormSchema = z.object({
invoice_number: z.string().optional(),
series: z.string().optional(),

View File

@ -1 +0,0 @@
export * from "./types";

View File

@ -1,122 +0,0 @@
import { z } from "zod/v4";
export const ProformaItemFormSchema = z.object({
description: z.string().max(2000).optional().default(""),
quantity: z.any(), //NumericStringSchema.optional(),
unit_amount: z.any(), //NumericStringSchema.optional(),
subtotal_amount: z.any(), //z.number(),
discount_percentage: z.any(), //NumericStringSchema.optional(),
discount_amount: z.number(),
taxable_amount: z.number(),
tax_codes: z.array(z.string()).default([]),
taxes_amount: z.number(),
total_amount: z.number(),
});
export const ProformaFormSchema = z.object({
invoice_number: z.string().optional(),
series: z.string().optional(),
invoice_date: z.string().optional(),
operation_date: z.string().optional(),
customer_id: z.string().optional(),
recipient: z
.object({
id: z.string().optional(),
name: z.string().optional(),
tin: z.string().optional(),
street: z.string().optional(),
street2: z.string().optional(),
city: z.string().optional(),
province: z.string().optional(),
postal_code: z.string().optional(),
country: z.string().optional(),
})
.optional(),
reference: z.string().optional(),
description: z.string().optional(),
notes: 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"),
taxes: z
.array(
z.object({
tax_code: z.string(),
tax_label: z.string(),
taxable_amount: z.number(),
taxes_amount: z.number(),
})
)
.optional(),
items: z.array(ProformaItemFormSchema).optional(),
subtotal_amount: z.number(),
items_discount_amount: z.number(),
discount_percentage: z.number(),
discount_amount: z.number(),
taxable_amount: z.number(),
taxes_amount: z.number(),
total_amount: z.number(),
});
export type ProformaFormData = z.infer<typeof ProformaFormSchema>;
export type ProformaItemFormData = z.infer<typeof ProformaItemFormSchema>;
export const defaultProformaItemFormData: ProformaItemFormData = {
description: "",
quantity: "",
unit_amount: "",
subtotal_amount: 0,
discount_percentage: "",
discount_amount: 0,
taxable_amount: 0,
tax_codes: ["iva_21"],
taxes_amount: 0,
total_amount: 0,
};
export const defaultProformaFormData: ProformaFormData = {
invoice_number: "",
series: "",
invoice_date: "",
operation_date: "",
reference: "",
description: "",
notes: "",
language_code: "es",
currency_code: "EUR",
items: [],
subtotal_amount: 0,
items_discount_amount: 0,
discount_amount: 0,
discount_percentage: 0,
taxable_amount: 0,
taxes_amount: 0,
total_amount: 0,
};

View File

@ -0,0 +1,2 @@
export * from "./proforma-header-fields-card";
export * from "./proforma-update-editor";

View File

@ -0,0 +1,119 @@
import {
Card,
CardContent,
CardHeader,
CardTitle,
Input,
Textarea,
} from "@repo/shadcn-ui/components";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "../../../../i18n";
import type { ProformaUpdateForm } from "../../entities";
export const ProformaHeaderFieldsCard = () => {
const { t } = useTranslation();
const { register, formState } = useFormContext<ProformaUpdateForm>();
return (
<Card>
<CardHeader>
<CardTitle>{t("proformas.update.header.title", "Cabecera")}</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="series">
{t("proformas.fields.series", "Serie")}
</label>
<Input id="series" {...register("series")} />
<FieldError message={formState.errors.series?.message} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="description">
{t("proformas.fields.description", "Descripción")}
</label>
<Input id="description" {...register("description")} />
<FieldError message={formState.errors.series?.message} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="customerId">
{t("proformas.fields.customer", "Cliente")}
</label>
<Input id="customerId" {...register("customerId")} />
<FieldError message={formState.errors.customerId?.message} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="invoiceDate">
{t("proformas.fields.invoice_date", "Fecha")}
</label>
<Input id="invoiceDate" type="date" {...register("invoiceDate")} />
<FieldError message={formState.errors.invoiceDate?.message} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="operationDate">
{t("proformas.fields.operation_date", "Fecha operación")}
</label>
<Input id="operationDate" type="date" {...register("operationDate")} />
<FieldError message={formState.errors.operationDate?.message} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="languageCode">
{t("proformas.fields.language", "Idioma")}
</label>
<Input id="languageCode" {...register("languageCode")} />
<FieldError message={formState.errors.languageCode?.message} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium" htmlFor="currencyCode">
{t("proformas.fields.currency", "Moneda")}
</label>
<Input id="currencyCode" {...register("currencyCode")} />
<FieldError message={formState.errors.currencyCode?.message} />
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-medium" htmlFor="reference">
{t("proformas.fields.reference", "Referencia")}
</label>
<Input id="reference" {...register("reference")} />
<FieldError message={formState.errors.reference?.message} />
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-medium" htmlFor="paymentMethod">
{t("proformas.fields.payment_method", "Forma de pago")}
</label>
<Input id="paymentMethod" {...register("paymentMethod")} />
<FieldError message={formState.errors.paymentMethod?.message} />
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-medium" htmlFor="notes">
{t("proformas.fields.notes", "Notas")}
</label>
<Textarea id="notes" rows={5} {...register("notes")} />
<FieldError message={formState.errors.notes?.message} />
</div>
</CardContent>
</Card>
);
};
type FieldErrorProps = {
message?: string;
};
const FieldError = ({ message }: FieldErrorProps) => {
if (!message) {
return null;
}
return <p className="text-sm text-destructive">{message}</p>;
};

View File

@ -0,0 +1,59 @@
// modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-update-editor.tsx
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components";
import { useTranslation } from "../../../../i18n";
import type { Proforma } from "../../../shared/entities";
import { ProformaHeaderFieldsCard } from "./proforma-header-fields-card";
type ProformaUpdateEditorProps = {
formId: string;
proforma?: Proforma;
isSubmitting: boolean;
onSubmit: React.FormEventHandler<HTMLFormElement>;
onReset: () => void;
};
export const ProformaUpdateEditor = ({
formId,
proforma,
isSubmitting,
onSubmit,
onReset,
}: ProformaUpdateEditorProps) => {
const { t } = useTranslation();
return (
<AppContent className="space-y-6">
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<h1 className="text-xl font-semibold">
{t("proformas.update.page_title", "Editar proforma")}
</h1>
{proforma?.reference ? (
<p className="text-sm text-muted-foreground">{proforma.reference}</p>
) : null}
</div>
<BackHistoryButton />
</div>
<form className="space-y-6" id={formId} onSubmit={onSubmit}>
<ProformaHeaderFieldsCard />
<div className="flex items-center justify-end gap-2">
<Button disabled={isSubmitting} onClick={onReset} type="button" variant="outline">
{t("common.reset", "Restablecer")}
</Button>
<Button disabled={isSubmitting} type="submit">
{isSubmitting ? t("common.saving", "Guardando...") : t("common.save", "Guardar")}
</Button>
</div>
</form>
</AppContent>
);
};

View File

@ -0,0 +1 @@
export * from "./proforma-update-skeleton";

View File

@ -0,0 +1,32 @@
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components";
import { useTranslation } from "../../../../i18n";
export const ProformaUpdateSkeleton = () => {
const { t } = useTranslation();
return (
<>
<AppContent>
<div className="flex items-center justify-between">
<div aria-hidden="true" className="space-y-2">
<div className="h-7 w-64 rounded-md bg-muted animate-pulse" />
<div className="h-5 w-96 rounded-md bg-muted animate-pulse" />
</div>
<div className="flex items-center gap-2">
<BackHistoryButton />
<Button aria-busy disabled>
{t("pages.update.submit")}
</Button>
</div>
</div>
<div aria-hidden="true" className="mt-6 grid gap-4">
<div className="h-10 w-full rounded-md bg-muted animate-pulse" />
<div className="h-10 w-full rounded-md bg-muted animate-pulse" />
<div className="h-28 w-full rounded-md bg-muted animate-pulse" />
</div>
<span className="sr-only">{t("pages.update.loading", "Cargando proforma...")}</span>
</AppContent>
</>
);
};

View File

@ -1,38 +1,57 @@
import { SpainTaxCatalogProvider } from "@erp/core";
import { useUrlParamId } from "@erp/core/hooks";
import { ErrorAlert } from "@erp/customers/components";
import { ErrorAlert } from "@erp/core/components";
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
import { useMemo } from "react";
import { FormProvider } from "react-hook-form";
import { useTranslation } from "../../../i18n";
import { useProformaUpdateController } from "../../controllers";
import { ProformaProvider } from "./context";
import { ProformaUpdateComp } from "./proforma-update-comp";
import { ProformaEditorSkeleton } from "./ui/components";
import { useTranslation } from "../../../../i18n";
import { useUpdateProformaPageController } from "../../controllers/use-update-proforma-page-controller";
import { ProformaUpdateEditor } from "../blocks";
import { ProformaUpdateSkeleton } from "../components";
export const ProformaUpdatePage = () => {
const initialProformaId = useUrlParamId();
const { t } = useTranslation();
const taxCatalog = useMemo(() => SpainTaxCatalogProvider(), []);
const {
form,
formId,
onSubmit,
resetForm,
const { updateCtrl } = useUpdateProformaPageController();
proformaData,
isLoading,
isLoadError,
loadError,
if (updateCtrl.isLoading) {
return <ProformaUpdateSkeleton />;
}
isUpdating,
isUpdateError,
updateError,
if (updateCtrl.isLoadError) {
return (
<AppContent>
<ErrorAlert
message={
updateCtrl.loadError instanceof Error
? updateCtrl.loadError.message
: t("proformas.update.load_error.message", "Inténtalo de nuevo más tarde")
}
title={t("proformas.update.load_error.title", "No se pudo cargar la proforma")}
/>
FormProvider,
} = useProformaUpdateController(initialProformaId, {});
<div className="flex items-center justify-end">
<BackHistoryButton />
</div>
</AppContent>
);
}
return (
<FormProvider {...updateCtrl.form}>
<ProformaUpdateEditor
formId={updateCtrl.formId}
isSubmitting={updateCtrl.isUpdating}
onReset={updateCtrl.resetForm}
onSubmit={updateCtrl.onSubmit}
proforma={updateCtrl.proforma}
/>
</FormProvider>
);
};
/*
if (isLoading) {
return <ProformaEditorSkeleton />;
@ -72,3 +91,4 @@ export const ProformaUpdatePage = () => {
</ProformaProvider>
);
};
*/

View File

@ -0,0 +1,15 @@
import { formHasAnyDirty, pickFormDirtyValues } from "@erp/core/client";
import type { FieldNamesMarkedBoolean } from "react-hook-form";
import type { ProformaUpdateForm, ProformaUpdatePatch } from "../entities";
export const buildProformaUpdatePatch = (
formData: ProformaUpdateForm,
dirtyFields: FieldNamesMarkedBoolean<ProformaUpdateForm>
): ProformaUpdatePatch => {
if (!formHasAnyDirty(dirtyFields)) {
return {};
}
return pickFormDirtyValues(formData, dirtyFields) as ProformaUpdatePatch;
};

View File

@ -0,0 +1,34 @@
import type { UpdateProformaByIdParams } from "../../shared/api";
import type { ProformaUpdatePatch } from "../entities";
export const buildUpdateProformaByIdParams = (
id: string,
patch: ProformaUpdatePatch
): UpdateProformaByIdParams => {
if (!id) {
throw new Error("proformaId is required");
}
const data: UpdateProformaByIdParams["data"] = {
series: patch.series,
invoice_date: patch.invoiceDate,
operation_date: patch.operationDate,
customer_id: patch.customerId,
reference: patch.reference,
description: patch.description,
notes: patch.notes,
language_code: patch.languageCode,
currency_code: patch.currencyCode,
};
return {
id,
data: {
...Object.fromEntries(Object.entries(data).filter(([, value]) => value !== undefined)),
},
};
};

View File

@ -0,0 +1,13 @@
import type { FieldErrors } from "react-hook-form";
import type { ProformaUpdateForm } from "../entities";
export const focusFirstProformaUpdateError = (errors: FieldErrors<ProformaUpdateForm>) => {
const firstKey = Object.keys(errors)[0] as keyof ProformaUpdateForm | undefined;
if (!firstKey) {
return;
}
document.querySelector<HTMLElement>(`[name="${String(firstKey)}"]`)?.focus();
};

View File

@ -0,0 +1,3 @@
export * from "./build-proforma-update-patch";
export * from "./build-update-proforma-by-id-params";
export * from "./focus-first-proforma-update-error";

View File

@ -1,3 +1,3 @@
export * from "./customer-create-form.entity";
export * from "./customer-create-form.schema";
export * from "./customer-create-form-default";
export * from "./customer-create-form-default.entity";

View File

@ -1,4 +1,3 @@
import { formHasAnyDirty } from "@erp/core/client";
import { useHookForm } from "@erp/core/hooks";
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
import { useEffect, useId, useMemo } from "react";
@ -18,6 +17,7 @@ import {
defaultCustomerUpdateForm,
} from "../entities";
import { buildCustomerUpdatePatch, buildUpdateCustomerByIdParams } from "../utils";
import { focusFirstCustomerUpdateError } from "../utils/focus-first-customer-update-error";
export interface UseCustomerUpdateControllerOptions {
onUpdated?(updated: Customer): void;
@ -84,23 +84,16 @@ export const useCustomerUpdateController = (
};
const submitHandler = form.handleSubmit(
async (formData) => {
async (formData: CustomerUpdateForm) => {
if (!customerId) {
showErrorToast(t("pages.update.error.title"), "Falta el ID del cliente");
return;
}
const { dirtyFields } = form.formState;
if (!formHasAnyDirty(dirtyFields)) {
showWarningToast(t("pages.update.error.no_changes"), "No hay cambios para guardar");
return;
}
const previousData = customerData;
const patchData = buildCustomerUpdatePatch(formData, dirtyFields);
const params: UpdateCustomerByIdParams = buildUpdateCustomerByIdParams(customerId, patchData);
const patchData = buildCustomerUpdatePatch(formData, form.formState.dirtyFields);
const params = buildUpdateCustomerByIdParams(customerId, patchData);
try {
// Enviamos cambios al servidor
@ -137,11 +130,7 @@ export const useCustomerUpdateController = (
}
},
(errors: FieldErrors<CustomerUpdateForm>) => {
const firstKey = Object.keys(errors)[0] as keyof CustomerUpdateForm | undefined;
if (firstKey) {
document.querySelector<HTMLElement>(`[name="${String(firstKey)}"]`)?.focus();
}
focusFirstCustomerUpdateError(errors);
showWarningToast(
t("forms.validation.title", "Revisa los campos"),

View File

@ -1,4 +1,4 @@
export * from "./customer-update-form.entity";
export * from "./customer-update-form.schema";
export * from "./customer-update-form-defaults";
export * from "./customer-update-form-default.entity";
export * from "./customer-update-patch.entity";

View File

@ -1,4 +1,4 @@
import { pickFormDirtyValues } from "@erp/core/client";
import { formHasAnyDirty, pickFormDirtyValues } from "@erp/core/client";
import type { FieldNamesMarkedBoolean } from "react-hook-form";
import type { CustomerUpdateForm, CustomerUpdatePatch } from "../entities";
@ -16,5 +16,8 @@ export const buildCustomerUpdatePatch = (
formData: CustomerUpdateForm,
dirtyFields: FieldNamesMarkedBoolean<CustomerUpdateForm>
): CustomerUpdatePatch => {
if (!formHasAnyDirty(dirtyFields)) {
return {};
}
return pickFormDirtyValues(formData, dirtyFields) as CustomerUpdatePatch;
};

View File

@ -23,10 +23,6 @@ export const buildUpdateCustomerByIdParams = (
id: string,
patchData: CustomerUpdatePatch
): UpdateCustomerByIdParams => {
if (!id) {
throw new Error("customerId is required");
}
return {
id,
data: patchData,

View File

@ -0,0 +1,13 @@
import type { FieldErrors } from "react-hook-form";
import type { CustomerUpdateForm } from "../entities";
export const focusFirstCustomerUpdateError = (errors: FieldErrors<CustomerUpdateForm>) => {
const firstKey = Object.keys(errors)[0] as keyof CustomerUpdateForm | undefined;
if (!firstKey) {
return;
}
document.querySelector<HTMLElement>(`[name="${String(firstKey)}"]`)?.focus();
};