Compare commits
No commits in common. "588aacd48aaba4dc872795899e68606a3f7953eb" and "ff6905b845616d8c9cdd3abe8036002847c34ba9" have entirely different histories.
588aacd48a
...
ff6905b845
@ -1,5 +1,5 @@
|
|||||||
const toSafeNumber = (value: string | number | null | undefined): number => {
|
const toSafeNumber = (value: number | null | undefined): number => {
|
||||||
return Number(value ?? 0);
|
return value ?? 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NumberHelper = {
|
export const NumberHelper = {
|
||||||
|
|||||||
@ -1,54 +1,28 @@
|
|||||||
type PickFormDirtyValuesOptions<T extends Record<string, any>> = {
|
|
||||||
replaceTopLevelArrayKeys?: Array<keyof T>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extrae solo los valores marcados como "dirty" por react-hook-form,
|
* Extrae solo los valores marcados como "dirty" por react-hook-form,
|
||||||
* respetando la estructura anidada de dirtyFields.
|
* respetando la estructura anidada de dirtyFields.
|
||||||
*
|
|
||||||
* Regla especial opcional: Si una key de primer nivel
|
|
||||||
* está configurada en `replaceTopLevelArrayKeys` y tiene
|
|
||||||
* cualquier dirty anidado, se incluye el array completo
|
|
||||||
* en lugar de intentar hacer patch recursivo
|
|
||||||
*/
|
*/
|
||||||
export function pickFormDirtyValues<T extends Record<string, any>>(
|
export function pickFormDirtyValues<T extends Record<string, any>>(
|
||||||
values: T,
|
values: T,
|
||||||
dirtyFields: Partial<Record<keyof T, any>>,
|
dirtyFields: Partial<Record<keyof T, any>>
|
||||||
options?: PickFormDirtyValuesOptions<T>
|
|
||||||
): Partial<T> {
|
): Partial<T> {
|
||||||
const result: Partial<T> = {};
|
const result: Partial<T> = {};
|
||||||
const replaceTopLevelArrayKeys = options?.replaceTopLevelArrayKeys ?? [];
|
|
||||||
|
|
||||||
for (const key in dirtyFields) {
|
for (const key in dirtyFields) {
|
||||||
if (!Object.hasOwn(dirtyFields, key)) continue;
|
if (!Object.hasOwn(dirtyFields, key)) continue;
|
||||||
|
|
||||||
const typedKey = key as keyof T;
|
const isDirty = dirtyFields[key];
|
||||||
const isDirty = dirtyFields[typedKey];
|
const value = values[key];
|
||||||
const value = values[typedKey];
|
|
||||||
|
|
||||||
if (isDirty === true) {
|
if (isDirty === true) {
|
||||||
result[typedKey] = value;
|
// 🔹 Campo "leaf": se ha tocado → copiar valor
|
||||||
continue;
|
result[key] = value;
|
||||||
}
|
} else if (typeof isDirty === "object" && isDirty !== null) {
|
||||||
|
// 🔹 Campo anidado: recursión
|
||||||
if (typeof isDirty !== "object" || isDirty === null) {
|
const nested = pickFormDirtyValues(value, isDirty);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldReplaceTopLevelArray =
|
|
||||||
replaceTopLevelArrayKeys.includes(typedKey) &&
|
|
||||||
Array.isArray(value) &&
|
|
||||||
formHasAnyDirty(isDirty);
|
|
||||||
|
|
||||||
if (shouldReplaceTopLevelArray) {
|
|
||||||
result[typedKey] = value;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nested = pickFormDirtyValues(value, isDirty as Partial<Record<keyof typeof value, any>>);
|
|
||||||
|
|
||||||
if (Object.keys(nested).length > 0) {
|
if (Object.keys(nested).length > 0) {
|
||||||
result[typedKey] = nested as T[keyof T];
|
result[key] = nested as any;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +37,7 @@ export function formHasAnyDirty(dirtyFields: Partial<Record<string, any>> | bool
|
|||||||
if (dirtyFields === false || dirtyFields == null) return false;
|
if (dirtyFields === false || dirtyFields == null) return false;
|
||||||
|
|
||||||
if (typeof dirtyFields === "object") {
|
if (typeof dirtyFields === "object") {
|
||||||
return Object.values(dirtyFields).some((value) => formHasAnyDirty(value));
|
return Object.values(dirtyFields).some((v) => formHasAnyDirty(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -3,12 +3,13 @@ import type { ICatalogs } from "@erp/core/api";
|
|||||||
import {
|
import {
|
||||||
CreateProformaInputMapper,
|
CreateProformaInputMapper,
|
||||||
type ICreateProformaInputMapper,
|
type ICreateProformaInputMapper,
|
||||||
|
type IUpdateProformaInputMapper,
|
||||||
UpdateProformaInputMapper,
|
UpdateProformaInputMapper,
|
||||||
} from "../mappers";
|
} from "../mappers";
|
||||||
|
|
||||||
export interface IProformaInputMappers {
|
export interface IProformaInputMappers {
|
||||||
createInputMapper: ICreateProformaInputMapper;
|
createInputMapper: ICreateProformaInputMapper;
|
||||||
updateInputMapper: UpdateProformaInputMapper;
|
updateInputMapper: IUpdateProformaInputMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildProformaInputMappers = (catalogs: ICatalogs): IProformaInputMappers => {
|
export const buildProformaInputMappers = (catalogs: ICatalogs): IProformaInputMappers => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { ITransactionManager } from "@erp/core/api";
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
|
||||||
import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
|
import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
|
||||||
import type { ICreateProformaInputMapper, UpdateProformaInputMapper } from "../mappers";
|
import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers";
|
||||||
import type {
|
import type {
|
||||||
IProformaCreator,
|
IProformaCreator,
|
||||||
IProformaFinder,
|
IProformaFinder,
|
||||||
@ -97,7 +97,7 @@ export function buildIssueProformaUseCase(deps: {
|
|||||||
|
|
||||||
export function buildUpdateProformaUseCase(deps: {
|
export function buildUpdateProformaUseCase(deps: {
|
||||||
updater: IProformaUpdater;
|
updater: IProformaUpdater;
|
||||||
dtoMapper: UpdateProformaInputMapper;
|
dtoMapper: IUpdateProformaInputMapper;
|
||||||
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||||
transactionManager: ITransactionManager;
|
transactionManager: ITransactionManager;
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { NumberHelper } from "@erp/core";
|
|
||||||
import { DiscountPercentage } from "@erp/core/api";
|
|
||||||
import {
|
import {
|
||||||
CurrencyCode,
|
CurrencyCode,
|
||||||
DomainError,
|
DomainError,
|
||||||
@ -15,14 +13,7 @@ import {
|
|||||||
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import type { UpdateProformaByIdRequestDTO } from "../../../../common/dto";
|
import type { UpdateProformaByIdRequestDTO } from "../../../../common/dto";
|
||||||
import {
|
import { InvoiceSerie, type ProformaPatchProps } from "../../../domain";
|
||||||
InvoiceSerie,
|
|
||||||
ItemAmount,
|
|
||||||
ItemDescription,
|
|
||||||
ItemQuantity,
|
|
||||||
type ProformaItemPatchProps,
|
|
||||||
type ProformaPatchProps,
|
|
||||||
} from "../../../domain";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UpdateProformaPropsMapper
|
* UpdateProformaPropsMapper
|
||||||
@ -46,10 +37,7 @@ export interface IUpdateProformaInputMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
|
export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
|
||||||
public map(
|
public map(dto: UpdateProformaByIdRequestDTO, params: { companyId: UniqueID }) {
|
||||||
dto: UpdateProformaByIdRequestDTO,
|
|
||||||
params: { companyId: UniqueID }
|
|
||||||
): Result<ProformaPatchProps> {
|
|
||||||
try {
|
try {
|
||||||
const errors: ValidationErrorDetail[] = [];
|
const errors: ValidationErrorDetail[] = [];
|
||||||
const props: ProformaPatchProps = {};
|
const props: ProformaPatchProps = {};
|
||||||
@ -140,149 +128,15 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dto.items) {
|
if (errors.length > 0) {
|
||||||
const itemsProps = this.mapItemsProps(dto, { errors });
|
return Result.fail(
|
||||||
props.items = itemsProps;
|
new ValidationErrorCollection("Proforma invoice props mapping failed (update)", errors)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.throwIfValidationErrors(errors);
|
|
||||||
|
|
||||||
return Result.ok(props);
|
return Result.ok(props);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
return Result.fail(new DomainError("Proforma proforma props mapping failed", { cause: err }));
|
return Result.fail(new DomainError("Proforma invoice props mapping failed", { cause: err }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
|
|
||||||
if (errors.length > 0) {
|
|
||||||
throw new ValidationErrorCollection("Customer proforma props mapping failed", errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private mapItemsProps(
|
|
||||||
dto: UpdateProformaByIdRequestDTO,
|
|
||||||
params: {
|
|
||||||
errors: ValidationErrorDetail[];
|
|
||||||
}
|
|
||||||
): ProformaItemPatchProps[] {
|
|
||||||
const itemsProps: ProformaItemPatchProps[] = [];
|
|
||||||
|
|
||||||
dto.items?.forEach((item, index) => {
|
|
||||||
const description = extractOrPushError(
|
|
||||||
maybeFromNullableResult(item.description, (v) => ItemDescription.create(v)),
|
|
||||||
`items[${index}].description`,
|
|
||||||
params.errors
|
|
||||||
);
|
|
||||||
|
|
||||||
const quantity = extractOrPushError(
|
|
||||||
maybeFromNullableResult(item.quantity, (v) =>
|
|
||||||
ItemQuantity.create({ value: NumberHelper.toSafeNumber(v) })
|
|
||||||
),
|
|
||||||
`items[${index}].quantity`,
|
|
||||||
params.errors
|
|
||||||
);
|
|
||||||
|
|
||||||
const unitAmount = extractOrPushError(
|
|
||||||
maybeFromNullableResult(item.unit_amount, (v) =>
|
|
||||||
ItemAmount.create({ value: NumberHelper.toSafeNumber(v) })
|
|
||||||
),
|
|
||||||
`items[${index}].unit_amount`,
|
|
||||||
params.errors
|
|
||||||
);
|
|
||||||
|
|
||||||
const discountPercentage = extractOrPushError(
|
|
||||||
maybeFromNullableResult(item.item_discount_percentage, (v) =>
|
|
||||||
DiscountPercentage.create({ value: NumberHelper.toSafeNumber(v.value) })
|
|
||||||
),
|
|
||||||
`items[${index}].discount_percentage`,
|
|
||||||
params.errors
|
|
||||||
);
|
|
||||||
|
|
||||||
/*const taxes = this.mapTaxesProps(item.taxes, {
|
|
||||||
itemIndex: index,
|
|
||||||
errors: params.errors,
|
|
||||||
});*/
|
|
||||||
|
|
||||||
this.throwIfValidationErrors(params.errors);
|
|
||||||
|
|
||||||
itemsProps.push({
|
|
||||||
description: description!,
|
|
||||||
quantity: quantity!,
|
|
||||||
unitAmount: unitAmount!,
|
|
||||||
itemDiscountPercentage: discountPercentage!,
|
|
||||||
//taxes,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return itemsProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Devuelve las propiedades de los impustos de una línea de detalle */
|
|
||||||
|
|
||||||
/*private mapTaxesProps(
|
|
||||||
taxesDTO: Pick<ProformaItemRequestDTO, "taxes">["taxes"],
|
|
||||||
params: { itemIndex: number; errors: ValidationErrorDetail[] }
|
|
||||||
): ProformaItemTaxesProps {
|
|
||||||
const { itemIndex, errors } = params;
|
|
||||||
|
|
||||||
const taxesProps: ProformaItemTaxesProps = {
|
|
||||||
iva: Maybe.none(),
|
|
||||||
retention: Maybe.none(),
|
|
||||||
rec: Maybe.none(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Normaliza: "" -> []
|
|
||||||
const taxStrCodes = taxesDTO
|
|
||||||
.split(",")
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter((s) => s.length > 0);
|
|
||||||
|
|
||||||
taxStrCodes.forEach((strCode, taxIndex) => {
|
|
||||||
const taxResult = Tax.createFromCode(strCode, this.taxCatalog);
|
|
||||||
|
|
||||||
if (!taxResult.isSuccess) {
|
|
||||||
errors.push({
|
|
||||||
path: `items[${itemIndex}].taxes[${taxIndex}]`,
|
|
||||||
message: taxResult.error.message,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tax = taxResult.data;
|
|
||||||
|
|
||||||
if (tax.isVATLike()) {
|
|
||||||
if (taxesProps.iva.isSome()) {
|
|
||||||
errors.push({
|
|
||||||
path: `items[${itemIndex}].taxes`,
|
|
||||||
message: "Multiple taxes for group VAT are not allowed",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
taxesProps.iva = Maybe.some(tax);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tax.isRetention()) {
|
|
||||||
if (taxesProps.retention.isSome()) {
|
|
||||||
errors.push({
|
|
||||||
path: `items[${itemIndex}].taxes`,
|
|
||||||
message: "Multiple taxes for group retention are not allowed",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
taxesProps.retention = Maybe.some(tax);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tax.isRec()) {
|
|
||||||
if (taxesProps.rec.isSome()) {
|
|
||||||
errors.push({
|
|
||||||
path: `items[${itemIndex}].taxes`,
|
|
||||||
message: "Multiple taxes for group rec are not allowed",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
taxesProps.rec = Maybe.some(tax);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.throwIfValidationErrors(errors);
|
|
||||||
|
|
||||||
return taxesProps;
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { UniqueID } from "@repo/rdx-ddd";
|
import type { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import { Proforma, type ProformaCreateProps } from "../../../domain";
|
import { type IProformaCreateProps, Proforma } from "../../../domain";
|
||||||
import type { IProformaRepository } from "../repositories";
|
import type { IProformaRepository } from "../repositories";
|
||||||
|
|
||||||
import type { IProformaNumberGenerator } from "./proforma-number-generator.interface";
|
import type { IProformaNumberGenerator } from "./proforma-number-generator.interface";
|
||||||
@ -9,7 +9,7 @@ import type { IProformaNumberGenerator } from "./proforma-number-generator.inter
|
|||||||
export interface IProformaCreatorParams {
|
export interface IProformaCreatorParams {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
props: Omit<ProformaCreateProps, "invoiceNumber">;
|
props: Omit<IProformaCreateProps, "invoiceNumber">;
|
||||||
transaction: unknown;
|
transaction: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export interface IProformaUpdater {
|
|||||||
update(params: {
|
update(params: {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
patchProps: ProformaPatchProps;
|
props: ProformaPatchProps;
|
||||||
transaction: unknown;
|
transaction: unknown;
|
||||||
}): Promise<Result<Proforma, Error>>;
|
}): Promise<Result<Proforma, Error>>;
|
||||||
}
|
}
|
||||||
@ -27,12 +27,12 @@ export class ProformaUpdater implements IProformaUpdater {
|
|||||||
async update(params: {
|
async update(params: {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
patchProps: ProformaPatchProps;
|
props: ProformaPatchProps;
|
||||||
transaction: unknown;
|
transaction: unknown;
|
||||||
}): Promise<Result<Proforma, Error>> {
|
}): Promise<Result<Proforma, Error>> {
|
||||||
const { companyId, id, patchProps, transaction } = params;
|
const { companyId, id, props, transaction } = params;
|
||||||
|
|
||||||
console.log("patchProps => ", patchProps);
|
console.log("props => ", props);
|
||||||
|
|
||||||
// Recuperar agregado existente
|
// Recuperar agregado existente
|
||||||
const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
|
const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
|
||||||
@ -44,12 +44,14 @@ export class ProformaUpdater implements IProformaUpdater {
|
|||||||
const proforma = existingResult.data;
|
const proforma = existingResult.data;
|
||||||
|
|
||||||
// Aplicar cambios en el agregado
|
// Aplicar cambios en el agregado
|
||||||
const updateResult = proforma.update(patchProps);
|
const updateResult = proforma.update(props);
|
||||||
|
|
||||||
if (updateResult.isFailure) {
|
if (updateResult.isFailure) {
|
||||||
return Result.fail(updateResult.error);
|
return Result.fail(updateResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(proforma.operationDate);
|
||||||
|
|
||||||
// Persistir cambios
|
// Persistir cambios
|
||||||
const saveResult = await this.repository.update(proforma, transaction);
|
const saveResult = await this.repository.update(proforma, transaction);
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { Result } from "@repo/rdx-utils";
|
|||||||
|
|
||||||
import type { UpdateProformaByIdRequestDTO } from "../../../../common";
|
import type { UpdateProformaByIdRequestDTO } from "../../../../common";
|
||||||
import type { ProformaPatchProps } from "../../../domain";
|
import type { ProformaPatchProps } from "../../../domain";
|
||||||
import type { UpdateProformaInputMapper } from "../mappers";
|
import type { IUpdateProformaInputMapper } from "../mappers";
|
||||||
import type { IProformaUpdater } from "../services";
|
import type { IProformaUpdater } from "../services";
|
||||||
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
|
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
|
||||||
|
|
||||||
@ -15,14 +15,14 @@ type UpdateProformaUseCaseInput = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UpdateProformaUseCaseDeps = {
|
type UpdateProformaUseCaseDeps = {
|
||||||
dtoMapper: UpdateProformaInputMapper;
|
dtoMapper: IUpdateProformaInputMapper;
|
||||||
updater: IProformaUpdater;
|
updater: IProformaUpdater;
|
||||||
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||||
transactionManager: ITransactionManager;
|
transactionManager: ITransactionManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class UpdateProformaUseCase {
|
export class UpdateProformaUseCase {
|
||||||
private readonly dtoMapper: UpdateProformaInputMapper;
|
private readonly dtoMapper: IUpdateProformaInputMapper;
|
||||||
private readonly updater: IProformaUpdater;
|
private readonly updater: IProformaUpdater;
|
||||||
private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||||
private readonly transactionManager: ITransactionManager;
|
private readonly transactionManager: ITransactionManager;
|
||||||
@ -60,7 +60,7 @@ export class UpdateProformaUseCase {
|
|||||||
const updateResult = await this.updater.update({
|
const updateResult = await this.updater.update({
|
||||||
companyId,
|
companyId,
|
||||||
id: proformaId,
|
id: proformaId,
|
||||||
patchProps,
|
props: patchProps,
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import {
|
|||||||
type IProformaItemCreateProps,
|
type IProformaItemCreateProps,
|
||||||
type IProformaItems,
|
type IProformaItems,
|
||||||
ProformaItem,
|
ProformaItem,
|
||||||
type ProformaItemPatchProps,
|
|
||||||
ProformaItems,
|
ProformaItems,
|
||||||
} from "../entities";
|
} from "../entities";
|
||||||
import { ProformaItemMismatch } from "../errors";
|
import { ProformaItemMismatch } from "../errors";
|
||||||
@ -57,10 +56,6 @@ export interface IProformaCreateProps {
|
|||||||
globalDiscountPercentage: DiscountPercentage;
|
globalDiscountPercentage: DiscountPercentage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProformaPatchProps = Partial<Omit<IProformaCreateProps, "companyId" | "items">> & {
|
|
||||||
items?: ProformaItemPatchProps[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IProformaTotals {
|
export interface IProformaTotals {
|
||||||
subtotalAmount: InvoiceAmount;
|
subtotalAmount: InvoiceAmount;
|
||||||
|
|
||||||
@ -105,6 +100,10 @@ export interface IProforma {
|
|||||||
totals(): IProformaTotals;
|
totals(): IProformaTotals;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProformaPatchProps = Partial<Omit<IProformaCreateProps, "companyId" | "items">> & {
|
||||||
|
//items?: ProformaItems;
|
||||||
|
};
|
||||||
|
|
||||||
export type InternalProformaProps = Omit<IProformaCreateProps, "items">;
|
export type InternalProformaProps = Omit<IProformaCreateProps, "items">;
|
||||||
|
|
||||||
export class Proforma extends AggregateRoot<InternalProformaProps> implements IProforma {
|
export class Proforma extends AggregateRoot<InternalProformaProps> implements IProforma {
|
||||||
@ -157,47 +156,9 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
|||||||
return new Proforma(props, items, id);
|
return new Proforma(props, items, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mutabilidad
|
private initializeItems(itemsProps: IProformaItemCreateProps[]): Result<void, Error> {
|
||||||
public update(patchProps: ProformaPatchProps): Result<Proforma, Error> {
|
|
||||||
const { items, ...otherProps } = patchProps;
|
|
||||||
|
|
||||||
const candidateProps: InternalProformaProps = {
|
|
||||||
...this.props,
|
|
||||||
...otherProps,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validacciones
|
|
||||||
|
|
||||||
// Aplicar cambios
|
|
||||||
Object.assign(this.props, candidateProps);
|
|
||||||
|
|
||||||
// Reemplazo de items (si se proporciona)
|
|
||||||
if (items) {
|
|
||||||
this.initializeItems(items);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
private initializeItems(
|
|
||||||
itemsProps: IProformaItemCreateProps[] | ProformaItemPatchProps[]
|
|
||||||
): Result<void, Error> {
|
|
||||||
this._items.reset();
|
|
||||||
|
|
||||||
for (const [index, itemProps] of itemsProps.entries()) {
|
for (const [index, itemProps] of itemsProps.entries()) {
|
||||||
const { languageCode, currencyCode, globalDiscountPercentage, ...restProps } = {
|
const itemResult = ProformaItem.create(itemProps);
|
||||||
languageCode: this.languageCode,
|
|
||||||
currencyCode: this.currencyCode,
|
|
||||||
globalDiscountPercentage: this.globalDiscountPercentage,
|
|
||||||
...itemProps,
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemResult = ProformaItem.create({
|
|
||||||
...restProps,
|
|
||||||
languageCode,
|
|
||||||
currencyCode,
|
|
||||||
globalDiscountPercentage,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (itemResult.isFailure) {
|
if (itemResult.isFailure) {
|
||||||
return Result.fail(itemResult.error);
|
return Result.fail(itemResult.error);
|
||||||
@ -285,6 +246,20 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
|||||||
return this.paymentMethod.isSome();
|
return this.paymentMethod.isSome();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mutabilidad
|
||||||
|
public update(patch: ProformaPatchProps): Result<Proforma, Error> {
|
||||||
|
const candidateProps: InternalProformaProps = {
|
||||||
|
...this.props,
|
||||||
|
...patch,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validacciones
|
||||||
|
|
||||||
|
Object.assign(this.props, candidateProps);
|
||||||
|
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
public issue(): Result<void, Error> {
|
public issue(): Result<void, Error> {
|
||||||
if (!this.props.status.canTransitionTo("issued")) {
|
if (!this.props.status.canTransitionTo("issued")) {
|
||||||
return Result.fail(
|
return Result.fail(
|
||||||
@ -299,6 +274,7 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
|||||||
this.props.status = InvoiceStatus.issued();
|
this.props.status = InvoiceStatus.issued();
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cálculos
|
// Cálculos
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -350,6 +326,25 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
|||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*public updateItem(itemId: UniqueID, props: IProformaItemProps): Result<void, Error> {
|
||||||
|
const item = this._items.find((i) => i.id.equals(itemId));
|
||||||
|
if (!item) {
|
||||||
|
return Result.fail(new Error("Item not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.update(props);
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/*public removeItem(itemId: UniqueID): Result<void, Error> {
|
||||||
|
const removed = this._items.removeWhere(i => i.id.equals(itemId));
|
||||||
|
|
||||||
|
if (!removed) {
|
||||||
|
return Result.fail(new Error("Item not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok();
|
||||||
|
}*/
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -42,11 +42,6 @@ export interface IProformaItemCreateProps {
|
|||||||
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
|
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProformaItemPatchProps = Omit<
|
|
||||||
IProformaItemCreateProps,
|
|
||||||
"globalDiscountPercentage" | "languageCode" | "currencyCode"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface IProformaItemTotals {
|
export interface IProformaItemTotals {
|
||||||
subtotalAmount: ItemAmount;
|
subtotalAmount: ItemAmount;
|
||||||
|
|
||||||
|
|||||||
@ -17,12 +17,12 @@ import {
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type IProformaCreateProps,
|
||||||
IssuedInvoiceItem,
|
IssuedInvoiceItem,
|
||||||
ItemAmount,
|
ItemAmount,
|
||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemQuantity,
|
ItemQuantity,
|
||||||
type Proforma,
|
type Proforma,
|
||||||
type ProformaCreateProps,
|
|
||||||
} from "../../../../../../domain";
|
} from "../../../../../../domain";
|
||||||
|
|
||||||
export interface ICustomerInvoiceItemDomainMapper
|
export interface ICustomerInvoiceItemDomainMapper
|
||||||
@ -62,7 +62,7 @@ export class CustomerInvoiceItemDomainMapper
|
|||||||
const { errors, index, attributes } = params as {
|
const { errors, index, attributes } = params as {
|
||||||
index: number;
|
index: number;
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
attributes: Partial<ProformaCreateProps>;
|
attributes: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemId = extractOrPushError(
|
const itemId = extractOrPushError(
|
||||||
@ -157,7 +157,7 @@ export class CustomerInvoiceItemDomainMapper
|
|||||||
const { errors, index } = params as {
|
const { errors, index } = params as {
|
||||||
index: number;
|
index: number;
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
attributes: Partial<ProformaCreateProps>;
|
attributes: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1) Valores escalares (atributos generales)
|
// 1) Valores escalares (atributos generales)
|
||||||
|
|||||||
@ -20,12 +20,12 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CustomerInvoiceItems,
|
CustomerInvoiceItems,
|
||||||
|
type IProformaCreateProps,
|
||||||
InvoiceNumber,
|
InvoiceNumber,
|
||||||
InvoicePaymentMethod,
|
InvoicePaymentMethod,
|
||||||
InvoiceSerie,
|
InvoiceSerie,
|
||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
Proforma,
|
Proforma,
|
||||||
type ProformaCreateProps,
|
|
||||||
} from "../../../../../../domain";
|
} from "../../../../../../domain";
|
||||||
import type {
|
import type {
|
||||||
CustomerInvoiceCreationAttributes,
|
CustomerInvoiceCreationAttributes,
|
||||||
@ -249,7 +249,7 @@ export class CustomerInvoiceDomainMapper
|
|||||||
items: itemsResults.data.getAll(),
|
items: itemsResults.data.getAll(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const invoiceProps: ProformaCreateProps = {
|
const invoiceProps: IProformaCreateProps = {
|
||||||
companyId: attributes.companyId!,
|
companyId: attributes.companyId!,
|
||||||
|
|
||||||
isProforma: attributes.isProforma,
|
isProforma: attributes.isProforma,
|
||||||
|
|||||||
@ -16,9 +16,9 @@ import {
|
|||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type IProformaCreateProps,
|
||||||
InvoiceRecipient,
|
InvoiceRecipient,
|
||||||
type Proforma,
|
type Proforma,
|
||||||
type ProformaCreateProps,
|
|
||||||
} from "../../../../../../domain";
|
} from "../../../../../../domain";
|
||||||
import type { CustomerInvoiceModel } from "../../../../sequelize";
|
import type { CustomerInvoiceModel } from "../../../../sequelize";
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ export class InvoiceRecipientDomainMapper {
|
|||||||
|
|
||||||
const { errors, attributes } = params as {
|
const { errors, attributes } = params as {
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
attributes: Partial<ProformaCreateProps>;
|
attributes: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const { isProforma } = attributes;
|
const { isProforma } = attributes;
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import {
|
|||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type IProformaCreateProps,
|
||||||
type Proforma,
|
type Proforma,
|
||||||
type ProformaCreateProps,
|
|
||||||
VerifactuRecord,
|
VerifactuRecord,
|
||||||
VerifactuRecordEstado,
|
VerifactuRecordEstado,
|
||||||
} from "../../../../../../domain";
|
} from "../../../../../../domain";
|
||||||
@ -43,7 +43,7 @@ export class CustomerInvoiceVerifactuDomainMapper
|
|||||||
): Result<Maybe<VerifactuRecord>, Error> {
|
): Result<Maybe<VerifactuRecord>, Error> {
|
||||||
const { errors, attributes } = params as {
|
const { errors, attributes } = params as {
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
attributes: Partial<ProformaCreateProps>;
|
attributes: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!source) {
|
if (!source) {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { Maybe, Result } from "@repo/rdx-utils";
|
|||||||
|
|
||||||
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common";
|
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common";
|
||||||
import {
|
import {
|
||||||
|
type IProformaCreateProps,
|
||||||
type IProformaItemCreateProps,
|
type IProformaItemCreateProps,
|
||||||
InvoiceNumber,
|
InvoiceNumber,
|
||||||
InvoicePaymentMethod,
|
InvoicePaymentMethod,
|
||||||
@ -28,7 +29,6 @@ import {
|
|||||||
ItemAmount,
|
ItemAmount,
|
||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemQuantity,
|
ItemQuantity,
|
||||||
type ProformaCreateProps,
|
|
||||||
} from "../../../../domain";
|
} from "../../../../domain";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -149,7 +149,7 @@ export class CreateProformaRequestMapper {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const proformaProps: Omit<ProformaCreateProps, "items"> & { items: IProformaItemCreateProps[] } = {
|
const proformaProps: Omit<IProformaCreateProps, "items"> & { items: IProformaItemCreateProps[] } = {
|
||||||
companyId,
|
companyId,
|
||||||
status: defaultStatus!,
|
status: defaultStatus!,
|
||||||
|
|
||||||
|
|||||||
@ -16,12 +16,12 @@ import {
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
type IProformaCreateProps,
|
||||||
type IProformaItemCreateProps,
|
type IProformaItemCreateProps,
|
||||||
ItemAmount,
|
ItemAmount,
|
||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemQuantity,
|
ItemQuantity,
|
||||||
type Proforma,
|
type Proforma,
|
||||||
type ProformaCreateProps,
|
|
||||||
ProformaItem,
|
ProformaItem,
|
||||||
ProformaItemTaxes,
|
ProformaItemTaxes,
|
||||||
type ProformaItemTaxesProps,
|
type ProformaItemTaxesProps,
|
||||||
@ -58,7 +58,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
|||||||
const { errors, index, parent } = params as {
|
const { errors, index, parent } = params as {
|
||||||
index: number;
|
index: number;
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
parent: Partial<ProformaCreateProps>;
|
parent: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemId = extractOrPushError(
|
const itemId = extractOrPushError(
|
||||||
@ -139,7 +139,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
|||||||
const { errors, index } = params as {
|
const { errors, index } = params as {
|
||||||
index: number;
|
index: number;
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
parent: Partial<ProformaCreateProps>;
|
parent: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1) Valores escalares (atributos generales)
|
// 1) Valores escalares (atributos generales)
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import { InvoiceRecipient, type ProformaCreateProps } from "../../../../../../domain";
|
import { type IProformaCreateProps, InvoiceRecipient } from "../../../../../../domain";
|
||||||
import type { CustomerInvoiceModel } from "../../../../../common";
|
import type { CustomerInvoiceModel } from "../../../../../common";
|
||||||
|
|
||||||
export class SequelizeProformaRecipientDomainMapper {
|
export class SequelizeProformaRecipientDomainMapper {
|
||||||
@ -28,7 +28,7 @@ export class SequelizeProformaRecipientDomainMapper {
|
|||||||
|
|
||||||
const { errors, parent } = params as {
|
const { errors, parent } = params as {
|
||||||
errors: ValidationErrorDetail[];
|
errors: ValidationErrorDetail[];
|
||||||
parent: Partial<ProformaCreateProps>;
|
parent: Partial<IProformaCreateProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _name = source.current_customer.name;
|
const _name = source.current_customer.name;
|
||||||
|
|||||||
@ -4,14 +4,11 @@ import { z } from "zod/v4";
|
|||||||
export const UpdateProformaItemRequestSchema = z.object({
|
export const UpdateProformaItemRequestSchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
position: z.string(),
|
position: z.string(),
|
||||||
description: z.string().default(""),
|
description: z.string().optional(),
|
||||||
quantity: NumericStringSchema.default(""),
|
quantity: NumericStringSchema.optional(),
|
||||||
unit_amount: NumericStringSchema.default(""),
|
unit_amount: NumericStringSchema.optional(),
|
||||||
item_discount_percentage: PercentageSchema.default({
|
item_discount_percentage: PercentageSchema.optional(),
|
||||||
value: "0",
|
taxes: z.string().optional(),
|
||||||
scale: "2",
|
|
||||||
}),
|
|
||||||
taxes: z.string().default(""),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateProformaByIdParamsRequestSchema = z.object({
|
export const UpdateProformaByIdParamsRequestSchema = z.object({
|
||||||
|
|||||||
@ -0,0 +1,142 @@
|
|||||||
|
import { formHasAnyDirty, pickFormDirtyValues } 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";
|
||||||
|
import { type FieldErrors, FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
|
import { useTranslation } from "../../../i18n";
|
||||||
|
import type { Proforma } from "../../api";
|
||||||
|
import { type ProformaFormData, ProformaFormSchema, defaultProformaFormData } from "../types";
|
||||||
|
|
||||||
|
export interface UseProformaUpdateControllerOptions {
|
||||||
|
onUpdated?(updated: Proforma): void;
|
||||||
|
successToasts?: boolean; // mostrar o no toast automáticcamente
|
||||||
|
|
||||||
|
onError?(error: Error, patchData: ReturnType<typeof pickFormDirtyValues>): void;
|
||||||
|
errorToasts?: boolean; // mostrar o no toast automáticcamente
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useProformaUpdateController = (
|
||||||
|
proformaId?: string,
|
||||||
|
options?: UseProformaUpdateControllerOptions
|
||||||
|
) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const formId = useId(); // id único por instancia
|
||||||
|
|
||||||
|
// 1) Estado de carga del cliente (query)
|
||||||
|
const {
|
||||||
|
data: proformaData,
|
||||||
|
isLoading,
|
||||||
|
isError: isLoadError,
|
||||||
|
error: loadError,
|
||||||
|
} = useProformaGetQuery(proformaId, { enabled: Boolean(proformaId) });
|
||||||
|
|
||||||
|
// 2) Estado de creación (mutación)
|
||||||
|
const {
|
||||||
|
mutateAsync,
|
||||||
|
isPending: isUpdating,
|
||||||
|
isError: isUpdateError,
|
||||||
|
error: updateError,
|
||||||
|
} = useProformaUpdateMutation();
|
||||||
|
|
||||||
|
const initialValues = useMemo(() => proformaData ?? defaultProformaFormData, [proformaData]);
|
||||||
|
|
||||||
|
// 3) Form hook
|
||||||
|
const form = useHookForm<ProformaFormData>({
|
||||||
|
resolverSchema: ProformaFormSchema,
|
||||||
|
initialValues,
|
||||||
|
disabled: isLoading || isUpdating,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Reiniciar el form al recibir datos */
|
||||||
|
useEffect(() => {
|
||||||
|
// keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render.
|
||||||
|
if (proformaData) form.reset(proformaData, { keepDirty: false });
|
||||||
|
}, [proformaData, form]);
|
||||||
|
|
||||||
|
/** Handlers */
|
||||||
|
|
||||||
|
const resetForm = () => form.reset(proformaData ?? defaultProformaFormData);
|
||||||
|
|
||||||
|
// Versión sincronizada
|
||||||
|
const submitHandler = form.handleSubmit(
|
||||||
|
async (formData) => {
|
||||||
|
if (!proformaId) {
|
||||||
|
showErrorToast(t("pages.update.error.title"), "Falta el ID de la proforma");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dirtyFields } = form.formState;
|
||||||
|
if (!formHasAnyDirty(dirtyFields)) {
|
||||||
|
showWarningToast(t("pages.update.error.no_changes"), "No hay cambios para guardar");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchData = pickFormDirtyValues(formData, dirtyFields);
|
||||||
|
const previousData = proformaData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Enviamos cambios al servidor
|
||||||
|
const updated = await mutateAsync({ id: proformaId, data: patchData });
|
||||||
|
|
||||||
|
// Ha ido bien -> actualizamos form con datos reales
|
||||||
|
// keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render.
|
||||||
|
form.reset(updated, { keepDirty: false });
|
||||||
|
|
||||||
|
if (options?.successToasts !== false) {
|
||||||
|
showSuccessToast(
|
||||||
|
t("pages.update.success.title", "Proforma modificada"),
|
||||||
|
t("pages.update.success.message", "Se ha modificado correctamente.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
options?.onUpdated?.(updated);
|
||||||
|
} catch (error: any) {
|
||||||
|
// Algo ha fallado -> revertimos cambios
|
||||||
|
form.reset(previousData ?? defaultProformaFormData);
|
||||||
|
if (options?.errorToasts !== false) {
|
||||||
|
showErrorToast(t("pages.update.error.title"), error.message);
|
||||||
|
}
|
||||||
|
options?.onError?.(error, patchData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(errors: FieldErrors<ProformaFormData>) => {
|
||||||
|
const firstKey = Object.keys(errors)[0] as keyof ProformaFormData | undefined;
|
||||||
|
if (firstKey) document.querySelector<HTMLElement>(`[name="${String(firstKey)}"]`)?.focus();
|
||||||
|
|
||||||
|
showWarningToast(
|
||||||
|
t("forms.validation.title", "Revisa los campos"),
|
||||||
|
t("forms.validation.message", "Hay errores de validación en el formulario.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Evento onSubmit ya preparado para el <form>
|
||||||
|
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.stopPropagation(); // <-- evita que el submit se propage por los padre en el árbol DOM
|
||||||
|
submitHandler(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
// form
|
||||||
|
form,
|
||||||
|
formId,
|
||||||
|
|
||||||
|
// handlers del form
|
||||||
|
onSubmit,
|
||||||
|
resetForm,
|
||||||
|
|
||||||
|
// carga de datos
|
||||||
|
proformaData,
|
||||||
|
isLoading,
|
||||||
|
isLoadError,
|
||||||
|
loadError,
|
||||||
|
|
||||||
|
// mutation
|
||||||
|
isUpdating,
|
||||||
|
isUpdateError,
|
||||||
|
updateError,
|
||||||
|
|
||||||
|
// Por comodidad
|
||||||
|
FormProvider,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import { formHasAnyDirty } from "@erp/core/client";
|
|
||||||
import { useHookForm } from "@erp/core/hooks";
|
import { useHookForm } from "@erp/core/hooks";
|
||||||
import type { CustomerSelectionOption } from "@erp/customers";
|
import type { CustomerSelectionOption } from "@erp/customers";
|
||||||
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers";
|
||||||
@ -115,19 +114,12 @@ export const useUpdateProformaController = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(form.formState.dirtyFields);
|
|
||||||
|
|
||||||
if (!formHasAnyDirty(form.formState.dirtyFields)) {
|
|
||||||
showWarningToast(
|
|
||||||
t("proformas.update.no_changes.title"),
|
|
||||||
t("proformas.update.no_changes.message")
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousData = proformaData;
|
const previousData = proformaData;
|
||||||
|
|
||||||
const patchData = buildProformaUpdatePatch(formData, form.formState.dirtyFields);
|
const patchData = buildProformaUpdatePatch(formData, form.formState.dirtyFields);
|
||||||
|
|
||||||
|
console.log(patchData);
|
||||||
|
|
||||||
const params = buildUpdateProformaByIdParams(proformaId, patchData);
|
const params = buildUpdateProformaByIdParams(proformaId, patchData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -140,7 +132,7 @@ export const useUpdateProformaController = (
|
|||||||
keepDirty: false,
|
keepDirty: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelectedCustomer(mapProformaToSelectedCustomer(updated));
|
setSelectedCustomer(mapProformaToSelectedCustomer(proformaData));
|
||||||
|
|
||||||
if (options?.successToasts !== false) {
|
if (options?.successToasts !== false) {
|
||||||
showSuccessToast(
|
showSuccessToast(
|
||||||
@ -161,8 +153,6 @@ export const useUpdateProformaController = (
|
|||||||
{ keepDirty: false }
|
{ keepDirty: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
setSelectedCustomer(previousData ? mapProformaToSelectedCustomer(previousData) : null);
|
|
||||||
|
|
||||||
if (options?.errorToasts !== false) {
|
if (options?.errorToasts !== false) {
|
||||||
showErrorToast(t("proformas.update.error.title"), normalizedError.message);
|
showErrorToast(t("proformas.update.error.title"), normalizedError.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import type { ProformaItemUpdateForm, ProformaItemUpdatePatch } from "../entities";
|
||||||
|
|
||||||
|
export const buildProformaItemsUpdatePatch = (
|
||||||
|
items: ProformaItemUpdateForm[]
|
||||||
|
): ProformaItemUpdatePatch[] => {
|
||||||
|
return items.map((item, index) => ({
|
||||||
|
id: item.id,
|
||||||
|
position: index,
|
||||||
|
description: item.description.trim(),
|
||||||
|
quantity: item.quantity,
|
||||||
|
unitAmount: item.unitAmount,
|
||||||
|
itemDiscountPercentage: item.itemDiscountPercentage,
|
||||||
|
}));
|
||||||
|
};
|
||||||
@ -3,6 +3,8 @@ import type { FieldNamesMarkedBoolean } from "react-hook-form";
|
|||||||
|
|
||||||
import type { ProformaUpdateForm, ProformaUpdatePatch } from "../entities";
|
import type { ProformaUpdateForm, ProformaUpdatePatch } from "../entities";
|
||||||
|
|
||||||
|
import { buildProformaItemsUpdatePatch } from "./build-proforma-items-update-patch";
|
||||||
|
|
||||||
export const buildProformaUpdatePatch = (
|
export const buildProformaUpdatePatch = (
|
||||||
formData: ProformaUpdateForm,
|
formData: ProformaUpdateForm,
|
||||||
dirtyFields: FieldNamesMarkedBoolean<ProformaUpdateForm>
|
dirtyFields: FieldNamesMarkedBoolean<ProformaUpdateForm>
|
||||||
@ -11,7 +13,10 @@ export const buildProformaUpdatePatch = (
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return pickFormDirtyValues(formData, dirtyFields, {
|
const itemsPatch = buildProformaItemsUpdatePatch(formData.items);
|
||||||
replaceTopLevelArrayKeys: ["items"],
|
|
||||||
}) satisfies ProformaUpdatePatch;
|
return {
|
||||||
|
...pickFormDirtyValues(formData, dirtyFields),
|
||||||
|
items: itemsPatch,
|
||||||
|
} as ProformaUpdatePatch;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -23,24 +23,12 @@ export const buildUpdateProformaByIdParams = (
|
|||||||
|
|
||||||
language_code: patch.languageCode,
|
language_code: patch.languageCode,
|
||||||
currency_code: patch.currencyCode,
|
currency_code: patch.currencyCode,
|
||||||
|
|
||||||
items: patch.items?.map((item) => ({
|
|
||||||
id: item.id,
|
|
||||||
position: String(item.position),
|
|
||||||
description: item.description,
|
|
||||||
quantity: item.quantity === null ? undefined : String(item.quantity),
|
|
||||||
unit_amount: item.unitAmount === null ? undefined : String(item.unitAmount),
|
|
||||||
item_discount_percentage:
|
|
||||||
item.itemDiscountPercentage === null
|
|
||||||
? undefined
|
|
||||||
: { value: String(item.itemDiscountPercentage), scale: "2" },
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
data: Object.fromEntries(
|
data: {
|
||||||
Object.entries(data).filter(([, value]) => value !== undefined)
|
...Object.fromEntries(Object.entries(data).filter(([, value]) => value !== undefined)),
|
||||||
) satisfies UpdateProformaByIdParams["data"],
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user