PROFORMAS UPDATE
This commit is contained in:
parent
982ed7d562
commit
bd209374bc
@ -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";
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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 }));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
@ -1 +0,0 @@
|
||||
export * from "./update-proforma.use-case";
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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 /> },
|
||||
|
||||
@ -2,4 +2,3 @@ export * from "../proformas";
|
||||
|
||||
export * from "./create";
|
||||
export * from "./list";
|
||||
export * from "./update";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./proforma-to-proforma-update-form.adapter";
|
||||
@ -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 ?? "",
|
||||
};
|
||||
};
|
||||
@ -1 +1,2 @@
|
||||
export * from "./use-proforma-update-page.controller";
|
||||
export * from "./use-update-proforma-controller";
|
||||
export * from "./use-update-proforma-page-controller";
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
};
|
||||
@ -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",
|
||||
};
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./types";
|
||||
@ -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,
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./proforma-header-fields-card";
|
||||
export * from "./proforma-update-editor";
|
||||
@ -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>;
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./proforma-update-skeleton";
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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)),
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -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();
|
||||
};
|
||||
@ -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";
|
||||
@ -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";
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -23,10 +23,6 @@ export const buildUpdateCustomerByIdParams = (
|
||||
id: string,
|
||||
patchData: CustomerUpdatePatch
|
||||
): UpdateCustomerByIdParams => {
|
||||
if (!id) {
|
||||
throw new Error("customerId is required");
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
data: patchData,
|
||||
|
||||
@ -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();
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user