Duplicado de proforma
This commit is contained in:
parent
fed29a71fd
commit
c46769e71e
@ -24,3 +24,4 @@
|
|||||||
- Ruta frontend: `/proformas/archived`
|
- Ruta frontend: `/proformas/archived`
|
||||||
- Reutiliza el listado compartido con scope `archived`.
|
- Reutiliza el listado compartido con scope `archived`.
|
||||||
- Mantiene selector de estado, paginación y orden.
|
- Mantiene selector de estado, paginación y orden.
|
||||||
|
- Muestra la acción de fila `Duplicar`; el duplicado vuelve a nacer activo.
|
||||||
|
|||||||
@ -24,3 +24,4 @@
|
|||||||
- Ruta frontend: `/proformas/deleted`
|
- Ruta frontend: `/proformas/deleted`
|
||||||
- Vista read-only V1.
|
- Vista read-only V1.
|
||||||
- No implementa restauración, purge ni force delete.
|
- No implementa restauración, purge ni force delete.
|
||||||
|
- No muestra la acción `Duplicar`.
|
||||||
|
|||||||
28
docs/customer-invoices/proforma-duplicate-contract.md
Normal file
28
docs/customer-invoices/proforma-duplicate-contract.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Duplicado de proformas
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
- `POST /proformas/:proforma_id/duplicate`
|
||||||
|
|
||||||
|
## Semántica
|
||||||
|
|
||||||
|
- Permite duplicar cualquier proforma no eliminada.
|
||||||
|
- La proforma origen puede estar archivada.
|
||||||
|
- Si la proforma origen está eliminada lógicamente, el endpoint responde como no disponible.
|
||||||
|
- El duplicado siempre nace en `draft`, activo, con `archived_at = NULL`, `deleted_at = NULL` y `linked_invoice_id = NULL`.
|
||||||
|
- La numeración siempre es nueva y se consume desde `document-series`.
|
||||||
|
- Se intenta reutilizar la misma serie documental del origen si sigue activa; si no, se usa la serie por defecto de `document_type=proforma`.
|
||||||
|
- `target_invoice_series_code` solo se conserva si sigue siendo una serie activa de `issued_invoice`; si no, pasa a `NULL`.
|
||||||
|
- Se copian datos comerciales y fiscales del origen.
|
||||||
|
- Se copian las líneas con nuevos IDs y se recalculan impuestos y totales con el motor actual.
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
- `201 Created`
|
||||||
|
- Devuelve el snapshot completo de la nueva proforma.
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
- Acción de fila `Duplicar` en `/proformas` y `/proformas/archived`.
|
||||||
|
- No muestra `Duplicar` en `/proformas/deleted`.
|
||||||
|
- Tras duplicar redirige a `/proformas/:id/edit`.
|
||||||
@ -24,3 +24,4 @@
|
|||||||
- Ruta frontend: `/proformas`
|
- Ruta frontend: `/proformas`
|
||||||
- Mantiene búsqueda, paginación, orden y selector de estado.
|
- Mantiene búsqueda, paginación, orden y selector de estado.
|
||||||
- No muestra selector visual de archivadas/todas.
|
- No muestra selector visual de archivadas/todas.
|
||||||
|
- Muestra la acción de fila `Duplicar`.
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import {
|
|||||||
ArchiveProformaByIdUseCase,
|
ArchiveProformaByIdUseCase,
|
||||||
ChangeStatusProformaUseCase,
|
ChangeStatusProformaUseCase,
|
||||||
CreateProformaUseCase,
|
CreateProformaUseCase,
|
||||||
|
DuplicateProformaByIdUseCase,
|
||||||
DeleteProformaByIdUseCase,
|
DeleteProformaByIdUseCase,
|
||||||
GetProformaByIdUseCase,
|
GetProformaByIdUseCase,
|
||||||
IssueProformaUseCase,
|
IssueProformaUseCase,
|
||||||
@ -120,6 +121,24 @@ export function buildCreateProformaUseCase(deps: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildDuplicateProformaByIdUseCase(deps: {
|
||||||
|
creator: IProformaCreator;
|
||||||
|
finder: IProformaFinder;
|
||||||
|
fullReadModelAssembler: IProformaFullReadModelAssembler;
|
||||||
|
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||||
|
documentSeriesServices: import("@erp/document-series/api").DocumentSeriesPublicServicesType;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}) {
|
||||||
|
return new DuplicateProformaByIdUseCase({
|
||||||
|
creator: deps.creator,
|
||||||
|
finder: deps.finder,
|
||||||
|
fullReadModelAssembler: deps.fullReadModelAssembler,
|
||||||
|
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||||
|
documentSeriesServices: deps.documentSeriesServices,
|
||||||
|
transactionManager: deps.transactionManager,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function buildIssueProformaUseCase(deps: {
|
export function buildIssueProformaUseCase(deps: {
|
||||||
publicServices: {
|
publicServices: {
|
||||||
issuedInvoiceServices: IIssuedInvoicePublicServices;
|
issuedInvoiceServices: IIssuedInvoicePublicServices;
|
||||||
|
|||||||
@ -0,0 +1,232 @@
|
|||||||
|
import type { ITransactionManager } from "@erp/core/api";
|
||||||
|
import type { DocumentSeriesPublicServicesType } from "@erp/document-series/api";
|
||||||
|
import { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
||||||
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { DuplicateProformaByIdParamsRequestDTO } from "../../../../common";
|
||||||
|
import { InvoiceSerie, InvoiceStatus, type ProformaRecipient } from "../../../domain";
|
||||||
|
import { InvoiceSeriesNotFoundError } from "../../invoice-series";
|
||||||
|
import type { ProformaCreateInputProps } from "../models";
|
||||||
|
import type {
|
||||||
|
IProformaCreator,
|
||||||
|
IProformaFinder,
|
||||||
|
IProformaFullReadModelAssembler,
|
||||||
|
} from "../services";
|
||||||
|
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
|
||||||
|
|
||||||
|
type DuplicateProformaByIdUseCaseInput = {
|
||||||
|
companyId: UniqueID;
|
||||||
|
params: DuplicateProformaByIdParamsRequestDTO;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class DuplicateProformaByIdUseCase {
|
||||||
|
public constructor(
|
||||||
|
private readonly deps: {
|
||||||
|
creator: IProformaCreator;
|
||||||
|
finder: IProformaFinder;
|
||||||
|
fullReadModelAssembler: IProformaFullReadModelAssembler;
|
||||||
|
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||||
|
documentSeriesServices: DocumentSeriesPublicServicesType;
|
||||||
|
transactionManager: ITransactionManager;
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async execute(params: DuplicateProformaByIdUseCaseInput) {
|
||||||
|
const proformaIdResult = UniqueID.create(params.params.proforma_id);
|
||||||
|
if (proformaIdResult.isFailure) {
|
||||||
|
return Result.fail(proformaIdResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.deps.transactionManager.complete(async (transaction: unknown) => {
|
||||||
|
try {
|
||||||
|
const sourceResult = await this.deps.finder.findProformaByIdIncludingArchived(
|
||||||
|
params.companyId,
|
||||||
|
proformaIdResult.data,
|
||||||
|
transaction
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sourceResult.isFailure) {
|
||||||
|
return Result.fail(sourceResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = sourceResult.data;
|
||||||
|
const duplicateId = UniqueID.generateNewID();
|
||||||
|
const proformaDateResult = UtcDate.createFromISO(new Date().toISOString().slice(0, 10));
|
||||||
|
|
||||||
|
if (proformaDateResult.isFailure) {
|
||||||
|
return Result.fail(proformaDateResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proformaSeriesCodeResult = await this.resolveDuplicateSeriesCode({
|
||||||
|
companyId: params.companyId,
|
||||||
|
sourceDocumentSeriesId: source.documentSeriesId,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (proformaSeriesCodeResult.isFailure) {
|
||||||
|
return Result.fail(proformaSeriesCodeResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetInvoiceSeriesCodeResult = await this.resolveValidTargetInvoiceSeriesCode({
|
||||||
|
companyId: params.companyId,
|
||||||
|
targetInvoiceSeriesCode: source.targetInvoiceSeriesCode,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (targetInvoiceSeriesCodeResult.isFailure) {
|
||||||
|
return Result.fail(targetInvoiceSeriesCodeResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const props: ProformaCreateInputProps = {
|
||||||
|
companyId: params.companyId,
|
||||||
|
status: InvoiceStatus.draft(),
|
||||||
|
proformaSeriesCode: proformaSeriesCodeResult.data,
|
||||||
|
targetInvoiceSeriesCode: targetInvoiceSeriesCodeResult.data,
|
||||||
|
proformaDate: proformaDateResult.data,
|
||||||
|
operationDate: Maybe.none(),
|
||||||
|
customerId: source.customerId,
|
||||||
|
recipient: Maybe.none<ProformaRecipient>(),
|
||||||
|
reference: source.reference,
|
||||||
|
description: source.description,
|
||||||
|
notes: source.notes,
|
||||||
|
languageCode: source.languageCode,
|
||||||
|
currencyCode: source.currencyCode,
|
||||||
|
linkedInvoiceId: Maybe.none(),
|
||||||
|
paymentMethodId: source.paymentMethodId,
|
||||||
|
taxRegimeCode: source.taxRegimeCode,
|
||||||
|
taxConfig: {
|
||||||
|
taxMode: source.taxConfig.taxMode,
|
||||||
|
defaultIvaCode: source.taxConfig.defaultIvaCode,
|
||||||
|
usesEquivalenceSurcharge: source.taxConfig.usesEquivalenceSurcharge,
|
||||||
|
defaultRecCode: source.taxConfig.defaultRecCode,
|
||||||
|
usesRetention: source.taxConfig.usesRetention,
|
||||||
|
defaultRetentionCode: source.taxConfig.defaultRetentionCode,
|
||||||
|
},
|
||||||
|
globalDiscountPercentage: source.globalDiscountPercentage,
|
||||||
|
archivedAt: Maybe.none(),
|
||||||
|
items: source.items.getAll().map((item) => ({
|
||||||
|
description: item.description,
|
||||||
|
quantity: item.quantity,
|
||||||
|
unitAmount: item.unitAmount,
|
||||||
|
itemDiscountPercentage: item.itemDiscountPercentage,
|
||||||
|
taxes: {
|
||||||
|
ivaCode: item.ivaCode(),
|
||||||
|
recCode: item.recCode(),
|
||||||
|
retentionCode: item.retentionCode(),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const createResult = await this.deps.creator.create({
|
||||||
|
companyId: params.companyId,
|
||||||
|
id: duplicateId,
|
||||||
|
props,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (createResult.isFailure) {
|
||||||
|
return Result.fail(createResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicatedResult = await this.deps.finder.findProformaById(
|
||||||
|
params.companyId,
|
||||||
|
createResult.data.id,
|
||||||
|
transaction
|
||||||
|
);
|
||||||
|
|
||||||
|
if (duplicatedResult.isFailure) {
|
||||||
|
return Result.fail(duplicatedResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const readModelResult = await this.deps.fullReadModelAssembler.assemble({
|
||||||
|
companyId: params.companyId,
|
||||||
|
proforma: duplicatedResult.data,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (readModelResult.isFailure) {
|
||||||
|
return Result.fail(readModelResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(this.deps.fullSnapshotBuilder.toOutput(readModelResult.data));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return Result.fail(error as Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveDuplicateSeriesCode(params: {
|
||||||
|
companyId: UniqueID;
|
||||||
|
sourceDocumentSeriesId: Maybe<string>;
|
||||||
|
transaction?: unknown;
|
||||||
|
}): Promise<Result<Maybe<InvoiceSerie>, Error>> {
|
||||||
|
const listResult = await this.deps.documentSeriesServices.listActiveSeries({
|
||||||
|
companyId: params.companyId.toString(),
|
||||||
|
documentType: "proforma",
|
||||||
|
transaction: params.transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (listResult.isFailure) {
|
||||||
|
return Result.fail(listResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceSeriesId = params.sourceDocumentSeriesId.getOrUndefined();
|
||||||
|
const sourceSeries = sourceSeriesId
|
||||||
|
? listResult.data.find((series) => series.id === sourceSeriesId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (sourceSeries) {
|
||||||
|
const seriesCodeResult = InvoiceSerie.create(sourceSeries.code);
|
||||||
|
if (seriesCodeResult.isFailure) {
|
||||||
|
return Result.fail(seriesCodeResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(Maybe.some(seriesCodeResult.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSeries = listResult.data.find((series) => series.is_default);
|
||||||
|
if (!defaultSeries) {
|
||||||
|
return Result.fail(
|
||||||
|
new InvoiceSeriesNotFoundError({
|
||||||
|
cause: "No active proforma document series is available.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSeriesCodeResult = InvoiceSerie.create(defaultSeries.code);
|
||||||
|
if (defaultSeriesCodeResult.isFailure) {
|
||||||
|
return Result.fail(defaultSeriesCodeResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(Maybe.some(defaultSeriesCodeResult.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveValidTargetInvoiceSeriesCode(params: {
|
||||||
|
companyId: UniqueID;
|
||||||
|
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
|
||||||
|
transaction?: unknown;
|
||||||
|
}): Promise<Result<Maybe<InvoiceSerie>, Error>> {
|
||||||
|
if (params.targetInvoiceSeriesCode.isNone()) {
|
||||||
|
return Result.ok(Maybe.none());
|
||||||
|
}
|
||||||
|
|
||||||
|
const listResult = await this.deps.documentSeriesServices.listActiveSeries({
|
||||||
|
companyId: params.companyId.toString(),
|
||||||
|
documentType: "issued_invoice",
|
||||||
|
transaction: params.transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (listResult.isFailure) {
|
||||||
|
return Result.fail(listResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetCode = params.targetInvoiceSeriesCode.unwrap().toPrimitive();
|
||||||
|
const isValid = listResult.data.some((series) => series.code === targetCode);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return Result.ok(Maybe.none());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(params.targetInvoiceSeriesCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
export * from "./archive-proforma-by-id.use-case";
|
export * from "./archive-proforma-by-id.use-case";
|
||||||
export * from "./change-status-proforma.use-case";
|
export * from "./change-status-proforma.use-case";
|
||||||
export * from "./create-proforma.use-case";
|
export * from "./create-proforma.use-case";
|
||||||
|
export * from "./duplicate-proforma-by-id.use-case";
|
||||||
export * from "./delete-proforma-by-id.use-case";
|
export * from "./delete-proforma-by-id.use-case";
|
||||||
export * from "./get-proforma-by-id.use-case";
|
export * from "./get-proforma-by-id.use-case";
|
||||||
export * from "./issue-proforma.use-case";
|
export * from "./issue-proforma.use-case";
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
type ChangeStatusProformaUseCase,
|
type ChangeStatusProformaUseCase,
|
||||||
type CreateProformaUseCase,
|
type CreateProformaUseCase,
|
||||||
type DeleteProformaByIdUseCase,
|
type DeleteProformaByIdUseCase,
|
||||||
|
type DuplicateProformaByIdUseCase,
|
||||||
type GetProformaByIdUseCase,
|
type GetProformaByIdUseCase,
|
||||||
type IIssuedInvoicePublicServices,
|
type IIssuedInvoicePublicServices,
|
||||||
type IssueProformaUseCase,
|
type IssueProformaUseCase,
|
||||||
@ -20,6 +21,7 @@ import {
|
|||||||
buildChangeStatusProformaUseCase,
|
buildChangeStatusProformaUseCase,
|
||||||
buildCreateProformaUseCase,
|
buildCreateProformaUseCase,
|
||||||
buildDeleteProformaByIdUseCase,
|
buildDeleteProformaByIdUseCase,
|
||||||
|
buildDuplicateProformaByIdUseCase,
|
||||||
buildGetProformaByIdUseCase,
|
buildGetProformaByIdUseCase,
|
||||||
buildIssueProformaUseCase,
|
buildIssueProformaUseCase,
|
||||||
buildListProformasUseCase,
|
buildListProformasUseCase,
|
||||||
@ -56,6 +58,7 @@ export type ProformasInternalDeps = {
|
|||||||
reportProformaPdf: () => ReportProformaPdfUseCase;
|
reportProformaPdf: () => ReportProformaPdfUseCase;
|
||||||
previewProformaReport: () => PreviewProformaReportUseCase;
|
previewProformaReport: () => PreviewProformaReportUseCase;
|
||||||
createProforma: () => CreateProformaUseCase;
|
createProforma: () => CreateProformaUseCase;
|
||||||
|
duplicateProforma: () => DuplicateProformaByIdUseCase;
|
||||||
issueProforma: (publicServices: {
|
issueProforma: (publicServices: {
|
||||||
issuedInvoiceServices: IIssuedInvoicePublicServices;
|
issuedInvoiceServices: IIssuedInvoicePublicServices;
|
||||||
}) => IssueProformaUseCase;
|
}) => IssueProformaUseCase;
|
||||||
@ -180,6 +183,16 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
|||||||
transactionManager,
|
transactionManager,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
duplicateProforma: () =>
|
||||||
|
buildDuplicateProformaByIdUseCase({
|
||||||
|
creator,
|
||||||
|
finder,
|
||||||
|
fullReadModelAssembler: readModelAssemblers.full,
|
||||||
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
|
documentSeriesServices,
|
||||||
|
transactionManager,
|
||||||
|
}),
|
||||||
|
|
||||||
updateProforma: () =>
|
updateProforma: () =>
|
||||||
buildUpdateProformaUseCase({
|
buildUpdateProformaUseCase({
|
||||||
updater,
|
updater,
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
import {
|
||||||
|
ExpressController,
|
||||||
|
forbidQueryFieldGuard,
|
||||||
|
requireAuthenticatedGuard,
|
||||||
|
requireCompanyContextGuard,
|
||||||
|
} from "@erp/core/api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DuplicateProformaByIdParamsRequestSchema,
|
||||||
|
DuplicateProformaByIdResponseSchema,
|
||||||
|
type DuplicateProformaByIdParamsRequestDTO,
|
||||||
|
} from "../../../../../common";
|
||||||
|
import type { DuplicateProformaByIdUseCase } from "../../../../application";
|
||||||
|
import { proformasApiErrorMapper } from "../proformas-api-error-mapper";
|
||||||
|
|
||||||
|
export class DuplicateProformaByIdController extends ExpressController {
|
||||||
|
public constructor(private readonly useCase: DuplicateProformaByIdUseCase) {
|
||||||
|
super();
|
||||||
|
this.errorMapper = proformasApiErrorMapper;
|
||||||
|
|
||||||
|
this.registerGuards(
|
||||||
|
requireAuthenticatedGuard(),
|
||||||
|
requireCompanyContextGuard(),
|
||||||
|
forbidQueryFieldGuard("companyId")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const companyId = this.getTenantId();
|
||||||
|
if (!companyId) {
|
||||||
|
return this.forbiddenError("Tenant ID not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = DuplicateProformaByIdParamsRequestSchema.parse(
|
||||||
|
this.req.params
|
||||||
|
) satisfies DuplicateProformaByIdParamsRequestDTO;
|
||||||
|
|
||||||
|
const result = await this.useCase.execute({
|
||||||
|
companyId,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.match(
|
||||||
|
(data) => this.created(DuplicateProformaByIdResponseSchema.parse(data)),
|
||||||
|
(err) => this.handleError(err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ import {
|
|||||||
ChangeStatusProformaByIdRequestSchema,
|
ChangeStatusProformaByIdRequestSchema,
|
||||||
CreateProformaRequestSchema,
|
CreateProformaRequestSchema,
|
||||||
DeleteProformaByIdParamsRequestSchema,
|
DeleteProformaByIdParamsRequestSchema,
|
||||||
|
DuplicateProformaByIdParamsRequestSchema,
|
||||||
GetProformaByIdRequestSchema,
|
GetProformaByIdRequestSchema,
|
||||||
IssueProformaByIdParamsRequestSchema,
|
IssueProformaByIdParamsRequestSchema,
|
||||||
LegacyIssueProformaByIdParamsRequestSchema,
|
LegacyIssueProformaByIdParamsRequestSchema,
|
||||||
@ -34,6 +35,7 @@ import type { IIssuedInvoicePublicServices } from "../../../application";
|
|||||||
|
|
||||||
import { ChangeStatusProformaController } from "./controllers/change-status-proforma.controller";
|
import { ChangeStatusProformaController } from "./controllers/change-status-proforma.controller";
|
||||||
import { CreateProformaController } from "./controllers/create-proforma.controller";
|
import { CreateProformaController } from "./controllers/create-proforma.controller";
|
||||||
|
import { DuplicateProformaByIdController } from "./controllers/duplicate-proforma-by-id.controller";
|
||||||
import { UpdateProformaController } from "./controllers/update-proforma.controller";
|
import { UpdateProformaController } from "./controllers/update-proforma.controller";
|
||||||
import { IssueProformaByIdRequestMapper } from "./mappers";
|
import { IssueProformaByIdRequestMapper } from "./mappers";
|
||||||
|
|
||||||
@ -146,6 +148,16 @@ export const proformasRouter = (params: StartParams) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/:proforma_id/duplicate",
|
||||||
|
validateRequest(DuplicateProformaByIdParamsRequestSchema, "params"),
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const useCase = deps.useCases.duplicateProforma();
|
||||||
|
const controller = new DuplicateProformaByIdController(useCase);
|
||||||
|
return controller.execute(req, res, next);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.delete(
|
router.delete(
|
||||||
"/:proforma_id",
|
"/:proforma_id",
|
||||||
validateRequest(DeleteProformaByIdParamsRequestSchema, "params"),
|
validateRequest(DeleteProformaByIdParamsRequestSchema, "params"),
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const DuplicateProformaByIdParamsRequestSchema = z.object({
|
||||||
|
proforma_id: z.uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DuplicateProformaByIdParamsRequestDTO = z.infer<
|
||||||
|
typeof DuplicateProformaByIdParamsRequestSchema
|
||||||
|
>;
|
||||||
@ -2,6 +2,7 @@ export * from "./archive-proforma-by-id.request.dto";
|
|||||||
export * from "./change-status-proforma-by-id.request.dto";
|
export * from "./change-status-proforma-by-id.request.dto";
|
||||||
export * from "./create-proforma.request.dto";
|
export * from "./create-proforma.request.dto";
|
||||||
export * from "./delete-proforma-by-id.request.dto";
|
export * from "./delete-proforma-by-id.request.dto";
|
||||||
|
export * from "./duplicate-proforma-by-id.request.dto";
|
||||||
export * from "./get-proforma-by-id.request.dto";
|
export * from "./get-proforma-by-id.request.dto";
|
||||||
export * from "./issue-proforma-by-id.request.dto";
|
export * from "./issue-proforma-by-id.request.dto";
|
||||||
export * from "./list-proformas.request.dto";
|
export * from "./list-proformas.request.dto";
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
import {
|
||||||
|
type GetProformaByIdResponseDTO,
|
||||||
|
GetProformaByIdResponseSchema,
|
||||||
|
} from "./get-proforma-by-id.response.dto";
|
||||||
|
|
||||||
|
export const DuplicateProformaByIdResponseSchema = GetProformaByIdResponseSchema;
|
||||||
|
export type DuplicateProformaByIdResponseDTO = GetProformaByIdResponseDTO;
|
||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./create-proforma.response.dto";
|
export * from "./create-proforma.response.dto";
|
||||||
|
export * from "./duplicate-proforma-by-id.response.dto";
|
||||||
export * from "./get-proforma-by-id.response.dto";
|
export * from "./get-proforma-by-id.response.dto";
|
||||||
export * from "./issue-proforma-by-id.response.dto";
|
export * from "./issue-proforma-by-id.response.dto";
|
||||||
export * from "./list-proformas.response.dto";
|
export * from "./list-proformas.response.dto";
|
||||||
|
|||||||
@ -175,6 +175,14 @@
|
|||||||
"available": "Available statuses",
|
"available": "Available statuses",
|
||||||
"unavailable": "Unavailable statuses"
|
"unavailable": "Unavailable statuses"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"duplicate": {
|
||||||
|
"success_title": "Proforma duplicated",
|
||||||
|
"success_description": "The proforma was duplicated successfully.",
|
||||||
|
"success_with_reference": "Proforma duplicated as {{reference}}.",
|
||||||
|
"error_title": "The proforma could not be duplicated",
|
||||||
|
"error_not_found_message": "The proforma no longer exists or is not available.",
|
||||||
|
"error_unknown_message": "An unexpected error occurred while duplicating the proforma."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
|
|||||||
@ -176,6 +176,14 @@
|
|||||||
"available": "Estados disponibles",
|
"available": "Estados disponibles",
|
||||||
"unavailable": "Estados no disponibles"
|
"unavailable": "Estados no disponibles"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"duplicate": {
|
||||||
|
"success_title": "Proforma duplicada",
|
||||||
|
"success_description": "La proforma se ha duplicado correctamente.",
|
||||||
|
"success_with_reference": "Proforma duplicada como {{reference}}.",
|
||||||
|
"error_title": "No se pudo duplicar la proforma",
|
||||||
|
"error_not_found_message": "La proforma ya no existe o no está disponible.",
|
||||||
|
"error_unknown_message": "Ha ocurrido un error inesperado al duplicar la proforma."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { useDownloadProformaPDFController } from "../../download-pdf/controller"
|
|||||||
import { useIssueProformaDialogController } from "../../issue-proforma";
|
import { useIssueProformaDialogController } from "../../issue-proforma";
|
||||||
import {
|
import {
|
||||||
type ProformaListRow,
|
type ProformaListRow,
|
||||||
|
useDuplicateProformaByIdMutation,
|
||||||
useProformaArchiveMutation,
|
useProformaArchiveMutation,
|
||||||
useProformaUnarchiveMutation,
|
useProformaUnarchiveMutation,
|
||||||
} from "../../shared";
|
} from "../../shared";
|
||||||
@ -25,6 +26,7 @@ export const useListProformasPageController = (scope: ProformaListScope) => {
|
|||||||
const issueDialogCtrl = useIssueProformaDialogController();
|
const issueDialogCtrl = useIssueProformaDialogController();
|
||||||
const changeStatusDialogCtrl = useChangeProformaStatusDialogController();
|
const changeStatusDialogCtrl = useChangeProformaStatusDialogController();
|
||||||
const archiveMutation = useProformaArchiveMutation();
|
const archiveMutation = useProformaArchiveMutation();
|
||||||
|
const duplicateMutation = useDuplicateProformaByIdMutation();
|
||||||
const unarchiveMutation = useProformaUnarchiveMutation();
|
const unarchiveMutation = useProformaUnarchiveMutation();
|
||||||
|
|
||||||
const downloadPDFCtrl = useDownloadProformaPDFController();
|
const downloadPDFCtrl = useDownloadProformaPDFController();
|
||||||
@ -126,6 +128,7 @@ export const useListProformasPageController = (scope: ProformaListScope) => {
|
|||||||
closeDialog: closeArchiveDialog,
|
closeDialog: closeArchiveDialog,
|
||||||
confirmArchive,
|
confirmArchive,
|
||||||
},
|
},
|
||||||
|
duplicateMutation,
|
||||||
deleteDialogCtrl,
|
deleteDialogCtrl,
|
||||||
issueDialogCtrl,
|
issueDialogCtrl,
|
||||||
changeStatusDialogCtrl,
|
changeStatusDialogCtrl,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { ProformaStatusBadge } from "../../components";
|
|||||||
export const ProformaHeader = ({ proforma }: { proforma: Proforma }) => {
|
export const ProformaHeader = ({ proforma }: { proforma: Proforma }) => {
|
||||||
const handleCopyTin = async () => {
|
const handleCopyTin = async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(proforma.recipient.tin);
|
await navigator.clipboard.writeText(proforma.recipient.tin ?? "");
|
||||||
} catch {
|
} catch {
|
||||||
// Silencio o toast fuera de este componente
|
// Silencio o toast fuera de este componente
|
||||||
}
|
}
|
||||||
@ -31,7 +31,7 @@ export const ProformaHeader = ({ proforma }: { proforma: Proforma }) => {
|
|||||||
|
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
<Badge className="gap-1" variant="outline">
|
<Badge className="gap-1" variant="outline">
|
||||||
{proforma.isProforma ? (
|
{proforma.recipient.tin ? (
|
||||||
<>
|
<>
|
||||||
<Building2Icon className="size-3" />
|
<Building2Icon className="size-3" />
|
||||||
Empresa
|
Empresa
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
} from "@repo/shadcn-ui/components";
|
} from "@repo/shadcn-ui/components";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
|
CopyIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
FileDownIcon,
|
FileDownIcon,
|
||||||
FileTextIcon,
|
FileTextIcon,
|
||||||
@ -38,6 +39,7 @@ type GridActionHandlers = {
|
|||||||
onEditClick?: (proforma: ProformaListRow) => void;
|
onEditClick?: (proforma: ProformaListRow) => void;
|
||||||
onIssueClick?: (proforma: ProformaListRow) => void;
|
onIssueClick?: (proforma: ProformaListRow) => void;
|
||||||
onArchiveClick?: (proforma: ProformaListRow) => void;
|
onArchiveClick?: (proforma: ProformaListRow) => void;
|
||||||
|
onDuplicateClick?: (proforma: ProformaListRow) => void;
|
||||||
onUnarchiveClick?: (proforma: ProformaListRow) => void;
|
onUnarchiveClick?: (proforma: ProformaListRow) => void;
|
||||||
onDeleteClick?: (proforma: ProformaListRow) => void;
|
onDeleteClick?: (proforma: ProformaListRow) => void;
|
||||||
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void;
|
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: ProformaStatus) => void;
|
||||||
@ -298,10 +300,11 @@ export function useProformasGridColumns(
|
|||||||
(proforma.status === PROFORMA_STATUS.DRAFT ||
|
(proforma.status === PROFORMA_STATUS.DRAFT ||
|
||||||
proforma.status === PROFORMA_STATUS.REJECTED);
|
proforma.status === PROFORMA_STATUS.REJECTED);
|
||||||
const availableTransitions = isNormalScope
|
const availableTransitions = isNormalScope
|
||||||
? PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? []
|
? (PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? [])
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const canEdit = isNormalScope && !isIssued && actionHandlers.onEditClick;
|
const canEdit = isNormalScope && !isIssued && actionHandlers.onEditClick;
|
||||||
|
const canDuplicate = !isDeletedScope && actionHandlers.onDuplicateClick;
|
||||||
const canIssue = isNormalScope && !isIssued && isApproved && actionHandlers.onIssueClick;
|
const canIssue = isNormalScope && !isIssued && isApproved && actionHandlers.onIssueClick;
|
||||||
const canDelete = !isDeletedScope && isDraft && actionHandlers.onDeleteClick;
|
const canDelete = !isDeletedScope && isDraft && actionHandlers.onDeleteClick;
|
||||||
const canDownloadPdf = !isDeletedScope && actionHandlers.onDownloadPdf;
|
const canDownloadPdf = !isDeletedScope && actionHandlers.onDownloadPdf;
|
||||||
@ -338,6 +341,31 @@ export function useProformasGridColumns(
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{canDuplicate && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
actionHandlers.onDuplicateClick?.(proforma);
|
||||||
|
}}
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
>
|
||||||
|
<CopyIcon className="size-4" />
|
||||||
|
<span className="sr-only">Duplicar</span>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TooltipContent>Duplicar</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
{isArchivable && actionHandlers.onArchiveClick && (
|
{isArchivable && actionHandlers.onArchiveClick && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@ -387,36 +415,38 @@ export function useProformasGridColumns(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cambiar estado */}
|
{/* Cambiar estado */}
|
||||||
{!isDeletedScope &&
|
{!(isDeletedScope || isIssued) &&
|
||||||
!isIssued &&
|
|
||||||
availableTransitions.length > 0 &&
|
availableTransitions.length > 0 &&
|
||||||
actionHandlers.onChangeStatusClick && (
|
actionHandlers.onChangeStatusClick && (
|
||||||
<TooltipProvider key={availableTransitions[0]}>
|
<TooltipProvider key={availableTransitions[0]}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger
|
<TooltipTrigger
|
||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
className="size-7 cursor-pointer text-muted-foreground hover:text-primary"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
actionHandlers.onChangeStatusClick?.(proforma, availableTransitions[0]);
|
actionHandlers.onChangeStatusClick?.(
|
||||||
}}
|
proforma,
|
||||||
size="icon"
|
availableTransitions[0]
|
||||||
variant="ghost"
|
);
|
||||||
>
|
}}
|
||||||
<RefreshCwIcon className="size-4" />
|
size="icon"
|
||||||
<span className="sr-only">Cambiar estado</span>
|
variant="ghost"
|
||||||
</Button>
|
>
|
||||||
}
|
<RefreshCwIcon className="size-4" />
|
||||||
/>
|
<span className="sr-only">Cambiar estado</span>
|
||||||
<TooltipContent>
|
</Button>
|
||||||
Cambiar a {t(`catalog.proformas.status.${availableTransitions[0]}.label`)}
|
}
|
||||||
</TooltipContent>
|
/>
|
||||||
</Tooltip>
|
<TooltipContent>
|
||||||
</TooltipProvider>
|
Cambiar a {t(`catalog.proformas.status.${availableTransitions[0]}.label`)}
|
||||||
)}
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Emitir factura: solo si approved */}
|
{/* Emitir factura: solo si approved */}
|
||||||
{canIssue && (
|
{canIssue && (
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
import { ErrorAlert, PageHeader, SimpleSearchInput } from "@erp/core/components";
|
||||||
import { useReturnToNavigation } from "@erp/core/hooks";
|
import { useReturnToNavigation } from "@erp/core/hooks";
|
||||||
|
import { showErrorToast, showSuccessToast } from "@repo/rdx-ui/helpers";
|
||||||
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
import { AppContent, BackHistoryButton } from "@repo/rdx-ui/components";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -93,6 +94,7 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
|||||||
issueDialogCtrl,
|
issueDialogCtrl,
|
||||||
changeStatusDialogCtrl,
|
changeStatusDialogCtrl,
|
||||||
archiveDialogCtrl,
|
archiveDialogCtrl,
|
||||||
|
duplicateMutation,
|
||||||
handleDownloadPDF,
|
handleDownloadPDF,
|
||||||
unarchiveDialogCtrl,
|
unarchiveDialogCtrl,
|
||||||
isPDFDownloading: isPdfDownloading,
|
isPDFDownloading: isPdfDownloading,
|
||||||
@ -108,8 +110,35 @@ const ScopedListProformasPage = ({ scope }: ScopedListProformasPageProps) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDuplicateClick = async (proforma: ProformaListRow) => {
|
||||||
|
try {
|
||||||
|
const duplicated = await duplicateMutation.mutateAsync({ proformaId: proforma.id });
|
||||||
|
|
||||||
|
showSuccessToast(
|
||||||
|
t("proformas.duplicate.success_title"),
|
||||||
|
duplicated.proformaReference
|
||||||
|
? t("proformas.duplicate.success_with_reference", {
|
||||||
|
reference: duplicated.proformaReference,
|
||||||
|
})
|
||||||
|
: t("proformas.duplicate.success_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
handleEditClick(duplicated.id);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const status = (error as { status?: number } | undefined)?.status;
|
||||||
|
|
||||||
|
showErrorToast(
|
||||||
|
t("proformas.duplicate.error_title"),
|
||||||
|
status === 404
|
||||||
|
? t("proformas.duplicate.error_not_found_message")
|
||||||
|
: t("proformas.duplicate.error_unknown_message")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const columns = useProformasGridColumns(scope, {
|
const columns = useProformasGridColumns(scope, {
|
||||||
onEditClick: (proforma) => handleEditClick(proforma.id),
|
onEditClick: (proforma) => handleEditClick(proforma.id),
|
||||||
|
onDuplicateClick: handleDuplicateClick,
|
||||||
onIssueClick: (proformaRow) =>
|
onIssueClick: (proformaRow) =>
|
||||||
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
|
issueDialogCtrl.openDialog(prepareIssueProformaTarget(proformaRow)),
|
||||||
onArchiveClick: archiveDialogCtrl.openDialog,
|
onArchiveClick: archiveDialogCtrl.openDialog,
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
|
import type { DuplicateProformaByIdResponseDTO } from "../../../../common";
|
||||||
|
|
||||||
|
export interface DuplicateProformaByIdParams {
|
||||||
|
proformaId: string;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DuplicateProformaByIdResult = DuplicateProformaByIdResponseDTO;
|
||||||
|
|
||||||
|
export const duplicateProformaById = (
|
||||||
|
dataSource: IDataSource,
|
||||||
|
params: DuplicateProformaByIdParams
|
||||||
|
): Promise<DuplicateProformaByIdResult> => {
|
||||||
|
const { proformaId, signal } = params;
|
||||||
|
|
||||||
|
if (!proformaId) throw new Error("proformaId is required");
|
||||||
|
|
||||||
|
return dataSource.custom<undefined, DuplicateProformaByIdResponseDTO>({
|
||||||
|
path: `proformas/${proformaId}/duplicate`,
|
||||||
|
method: "post",
|
||||||
|
data: undefined,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@ export * from "./archive-proforma-by-id.api";
|
|||||||
export * from "./change-proforma-status-by-id.api";
|
export * from "./change-proforma-status-by-id.api";
|
||||||
export * from "./create-proforma.api";
|
export * from "./create-proforma.api";
|
||||||
export * from "./delete-proforma-by-id.api";
|
export * from "./delete-proforma-by-id.api";
|
||||||
|
export * from "./duplicate-proforma-by-id.api";
|
||||||
export * from "./get-proforma-by-id.api";
|
export * from "./get-proforma-by-id.api";
|
||||||
export * from "./issue-proforma-by-id.api";
|
export * from "./issue-proforma-by-id.api";
|
||||||
export * from "./list-document-series.api";
|
export * from "./list-document-series.api";
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export * from "./keys";
|
export * from "./keys";
|
||||||
export * from "./use-document-series-query";
|
export * from "./use-document-series-query";
|
||||||
|
export * from "./use-duplicate-proforma-by-id-mutation";
|
||||||
export * from "./use-proforma-archive-mutation";
|
export * from "./use-proforma-archive-mutation";
|
||||||
export * from "./use-proforma-change-status-mutation";
|
export * from "./use-proforma-change-status-mutation";
|
||||||
export * from "./use-proforma-create-mutation";
|
export * from "./use-proforma-create-mutation";
|
||||||
|
|||||||
@ -44,6 +44,7 @@ export const PROFORMA_QUERY_KEY = (proformaId?: string): QueryKey => [
|
|||||||
export const CREATE_PROFORMA_MUTATION_KEY = ["proformas:create"] as const;
|
export const CREATE_PROFORMA_MUTATION_KEY = ["proformas:create"] as const;
|
||||||
export const UPDATE_PROFORMA_MUTATION_KEY = ["proformas:update"] as const;
|
export const UPDATE_PROFORMA_MUTATION_KEY = ["proformas:update"] as const;
|
||||||
export const DELETE_PROFORMA_MUTATION_KEY = ["proformas:delete"] as const;
|
export const DELETE_PROFORMA_MUTATION_KEY = ["proformas:delete"] as const;
|
||||||
|
export const DUPLICATE_PROFORMA_MUTATION_KEY = ["proformas:duplicate"] as const;
|
||||||
export const ARCHIVE_PROFORMA_MUTATION_KEY = ["proformas:archive"] as const;
|
export const ARCHIVE_PROFORMA_MUTATION_KEY = ["proformas:archive"] as const;
|
||||||
export const UNARCHIVE_PROFORMA_MUTATION_KEY = ["proformas:unarchive"] as const;
|
export const UNARCHIVE_PROFORMA_MUTATION_KEY = ["proformas:unarchive"] as const;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { useDataSource } from "@erp/core/hooks";
|
||||||
|
import { type DefaultError, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { GetProformaByIdAdapter } from "../adapters";
|
||||||
|
import { type DuplicateProformaByIdParams, duplicateProformaById } from "../api";
|
||||||
|
import type { Proforma } from "../entities";
|
||||||
|
|
||||||
|
import { DUPLICATE_PROFORMA_MUTATION_KEY } from "./keys";
|
||||||
|
import { syncCreatedProformaCaches } from "./proforma-cache-strategy";
|
||||||
|
|
||||||
|
export const useDuplicateProformaByIdMutation = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const dataSource = useDataSource();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: DUPLICATE_PROFORMA_MUTATION_KEY,
|
||||||
|
mutationFn: async (params: DuplicateProformaByIdParams) => {
|
||||||
|
const dto = await duplicateProformaById(dataSource, params);
|
||||||
|
return GetProformaByIdAdapter.fromDto(dto);
|
||||||
|
},
|
||||||
|
onSuccess: async (proforma) => {
|
||||||
|
await syncCreatedProformaCaches(queryClient, proforma);
|
||||||
|
},
|
||||||
|
}) as ReturnType<typeof useMutation<Proforma, DefaultError, DuplicateProformaByIdParams>>;
|
||||||
|
};
|
||||||
@ -75,7 +75,7 @@ export function DataTableToolbar<TData>({
|
|||||||
// Render principal
|
// Render principal
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex items-center justify-between gap-2 px-2 py-2 bg-transparent", className)}
|
className={cn("flex items-center justify-between gap-2 px-2 py-2 bg-background", className)}
|
||||||
>
|
>
|
||||||
{/* IZQUIERDA: acciones + contador */}
|
{/* IZQUIERDA: acciones + contador */}
|
||||||
<div className="flex flex-1 items-center gap-3 flex-wrap">
|
<div className="flex flex-1 items-center gap-3 flex-wrap">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user