Borrado de proformas draft

This commit is contained in:
David Arranz 2026-07-29 12:43:54 +02:00
parent 69bfbbb615
commit 718e8b13d9
32 changed files with 420 additions and 110 deletions

View File

@ -99,6 +99,12 @@ Adaptando nombres exactos al contrato publico actual:
- al existir lineas valoradas, `update` recalcula `taxes` y `totals`
- `issue` genera la `issued_invoice` materializada usando `target_invoice_series_code` si existe y, si no, la default activa de `issued_invoice`
## Relacion con borrado
- una proforma creada en `draft` puede borrarse logicamente mas adelante
- ese borrado no modifica `tax_config`, impuestos, totales ni numeracion historica
- la referencia visible consumida por create no vuelve a reutilizarse aunque la proforma se borre
## Wording de UI
La UI de create debe hablar de:

View File

@ -0,0 +1,40 @@
# Proforma Delete Contract
## Objetivo
Definir el borrado funcional de proformas sin romper trazabilidad ni numeracion.
## Reglas funcionales
- solo se pueden borrar proformas en estado `draft`
- `sent`, `approved`, `rejected` e `issued` devuelven conflicto
- `rejected` no se borra; queda reservado para una futura operacion de archivado
- el borrado es logico mediante `deleted_at`
- la numeracion no se reutiliza
- no se decrementa `document_series.next_number`
- no se borran fisicamente `proforma_items` ni `proforma_taxes`
## Validaciones defensivas
- si `linked_invoice_id` tiene valor, el borrado se bloquea
- si existe `issued_invoices.source_proforma_id = proforma.id`, el borrado se bloquea
## Contrato REST
- endpoint: `DELETE /proformas/:proforma_id`
- respuesta satisfactoria: `204 No Content`
- `404 Not Found` si no existe, no pertenece a la empresa activa o ya esta borrada
- `409 Conflict` si existe pero no cumple las reglas de borrado
## Efecto operativo
- `GET /proformas/:id` debe tratar la proforma borrada como inexistente
- `GET /proformas` no debe incluir proformas borradas
- `PUT /proformas/:id` no debe editar proformas borradas
- `POST /proformas/:id/issue` no debe emitir proformas borradas
## UI
- la accion `Eliminar` solo debe mostrarse para proformas `draft`
- la confirmacion debe advertir que la referencia no se reutilizara
- en esta fase la accion solo esta disponible en el listado

View File

@ -74,6 +74,13 @@ En proformas:
- la UI nunca debe consumir numeracion directa desde `POST /document-series/assign-next`
- los impuestos y totales reales se recalculan cuando la proforma ya tiene lineas en update
## Regla de borrado y numeracion
- borrar una proforma draft no libera ni reutiliza `proforma_reference`
- el siguiente alta consume el siguiente numero disponible de `document-series`
- no se decrementa `document_series.next_number`
- las proformas `rejected` no se borran; quedan para una futura operacion de archivado
## Validacion defensiva
Si el request informa `target_invoice_series_code`:

View File

@ -165,6 +165,15 @@ Preparar el esquema fisico para separar:
- update sigue siendo el punto donde se anaden lineas y se materializan impuestos/totales reales
- la UI deja de prometer que la fiscalidad queda "aplicada" en create y pasa a hablar de valores iniciales
## Estado Fase 2G
- `DELETE /proformas/:proforma_id` vuelve a estar operativo sobre persistencia V2
- el borrado es logico mediante `deleted_at`
- solo `draft` puede borrarse
- `rejected` sigue fuera del alcance y queda reservado para archivado futuro
- la validacion de borrado comprueba tambien `linked_invoice_id` y `issued_invoices.source_proforma_id`
- la numeracion de `document-series` no se reutiliza ni se decrementa tras un borrado
## Rollback recomendado
- no aplicar nada en produccion en esta fase

View File

@ -10,6 +10,7 @@ Este directorio mezcla SQL vigentes de soporte con SQL historicos/transicionales
- `add-proforma-tax-config-columns.sql`
- `fix-proforma-series-semantics.sql`
- `validate-proforma-tax-config.sql`
- `validate-proforma-soft-delete.sql`
## SQL históricos / transicionales

View File

@ -0,0 +1,14 @@
SELECT id, proforma_reference, status, deleted_at
FROM proformas
WHERE deleted_at IS NOT NULL
AND status <> 'draft';
SELECT id, proforma_reference, deleted_at
FROM proformas
WHERE deleted_at IS NOT NULL
ORDER BY deleted_at DESC
LIMIT 20;
SELECT ds.document_type, ds.code, ds.next_number
FROM document_series ds
WHERE ds.document_type = 'proforma';

View File

@ -1,5 +1,6 @@
export * from "./proforma-catalog-resolvers.di";
export * from "./proforma-creator.di";
export * from "./proforma-deleter.di";
export * from "./proforma-finder.di";
export * from "./proforma-input-mappers.di";
export * from "./proforma-issuer.di";

View File

@ -0,0 +1,8 @@
import { type IProformaDeleter, ProformaDeleter } from "../services";
import type { IProformaRepository } from "../repositories";
export function buildProformaDeleter(deps: { repository: IProformaRepository }): IProformaDeleter {
return new ProformaDeleter({
repository: deps.repository,
});
}

View File

@ -5,6 +5,7 @@ import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../
import type { IProformaRepository } from "../repositories";
import type {
ICompanyReportProfileFinder,
IProformaDeleter,
IProformaCreator,
IProformaFinder,
IProformaFullReadModelAssembler,
@ -23,6 +24,7 @@ import type {
import {
ChangeStatusProformaUseCase,
CreateProformaUseCase,
DeleteProformaByIdUseCase,
GetProformaByIdUseCase,
IssueProformaUseCase,
ListProformasUseCase,
@ -163,10 +165,15 @@ export function buildUpdateProformaUseCase(deps: {
});
}
/*
export function buildDeleteProformaUseCase(deps: { finder: IProformaFinder }) {
return new DeleteProformaUseCase(deps.finder);
}*/
export function buildDeleteProformaByIdUseCase(deps: {
deleter: IProformaDeleter;
transactionManager: ITransactionManager;
}) {
return new DeleteProformaByIdUseCase({
deleter: deps.deleter,
transactionManager: deps.transactionManager,
});
}
export function buildChangeStatusProformaUseCase(deps: {
statusChanger: IProformaStatusChanger;

View File

@ -34,6 +34,12 @@ export interface IProformaRepository {
transaction: unknown
): Promise<Result<boolean, Error>>;
existsIssuedInvoiceBySourceProformaIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: unknown
): Promise<Result<boolean, Error>>;
updateStatusByIdInCompany(
companyId: UniqueID,
id: UniqueID,

View File

@ -4,6 +4,7 @@ export * from "./company-report-profile-finder";
export * from "./proforma-creator";
export * from "./proforma-document-generator.interface";
export * from "./proforma-document-properties-factory";
export * from "./proforma-deleter";
export * from "./proforma-finder";
export * from "./proforma-issuer";
export * from "./proforma-number-generator.interface";

View File

@ -0,0 +1,73 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { ProformaCannotBeDeletedError } from "../../../domain";
import type { IProformaRepository } from "../repositories";
export interface IProformaDeleter {
delete(params: {
companyId: UniqueID;
id: UniqueID;
transaction: unknown;
}): Promise<Result<void, Error>>;
}
export class ProformaDeleter implements IProformaDeleter {
public constructor(
private readonly deps: {
repository: IProformaRepository;
}
) {}
public async delete(params: {
companyId: UniqueID;
id: UniqueID;
transaction: unknown;
}): Promise<Result<void, Error>> {
const proformaResult = await this.deps.repository.getByIdInCompany(
params.companyId,
params.id,
params.transaction
);
if (proformaResult.isFailure) {
return Result.fail(proformaResult.error);
}
const proforma = proformaResult.data;
const canDeleteResult = proforma.ensureCanBeDeleted();
if (canDeleteResult.isFailure) {
return Result.fail(canDeleteResult.error);
}
const hasIssuedInvoiceResult =
await this.deps.repository.existsIssuedInvoiceBySourceProformaIdInCompany(
params.companyId,
params.id,
params.transaction
);
if (hasIssuedInvoiceResult.isFailure) {
return Result.fail(hasIssuedInvoiceResult.error);
}
if (hasIssuedInvoiceResult.data) {
return Result.fail(
new ProformaCannotBeDeletedError(params.id.toString(), "Issued proformas cannot be deleted.")
);
}
const deleteResult = await this.deps.repository.deleteByIdInCompany(
params.companyId,
params.id,
params.transaction
);
if (deleteResult.isFailure) {
return Result.fail(deleteResult.error);
}
return Result.ok();
}
}

View File

@ -0,0 +1,39 @@
import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IProformaDeleter } from "../services";
type DeleteProformaByIdUseCaseInput = {
companyId: UniqueID;
proforma_id: string;
};
export class DeleteProformaByIdUseCase {
public constructor(
private readonly deps: {
deleter: IProformaDeleter;
transactionManager: ITransactionManager;
}
) {}
public execute(params: DeleteProformaByIdUseCaseInput) {
const proformaIdResult = UniqueID.create(params.proforma_id);
if (proformaIdResult.isFailure) {
return Result.fail(proformaIdResult.error);
}
return this.deps.transactionManager.complete(async (transaction) => {
try {
return await this.deps.deleter.delete({
companyId: params.companyId,
id: proformaIdResult.data,
transaction,
});
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -1,6 +1,6 @@
export * from "./change-status-proforma.use-case";
export * from "./create-proforma.use-case";
//export * from "./delete-proforma.use-case";
export * from "./delete-proforma-by-id.use-case";
export * from "./get-proforma-by-id.use-case";
export * from "./issue-proforma.use-case";
export * from "./list-proformas.use-case";

View File

@ -34,6 +34,7 @@ import {
type ProformaRecipient,
type ProformaTaxConfig,
} from "../value-objects";
import { ProformaCannotBeDeletedError } from "../errors";
export interface IProformaCreateProps {
companyId: UniqueID;
@ -386,6 +387,30 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
return Result.ok();
}
public ensureCanBeDeleted(): Result<void, Error> {
if (!this.status.isDraft()) {
return Result.fail(
new ProformaCannotBeDeletedError(
this.id.toString(),
this.status.isIssued()
? "Issued proformas cannot be deleted."
: "Only draft proformas can be deleted."
)
);
}
if (this.linkedInvoiceId.isSome()) {
return Result.fail(
new ProformaCannotBeDeletedError(
this.id.toString(),
"Issued proformas cannot be deleted."
)
);
}
return Result.ok();
}
private validateCanBeIssued(): Result<void, Error> {
if (this.description.isNone()) {
return Result.fail(

View File

@ -8,5 +8,6 @@ export class ProformaCannotBeDeletedError extends DomainError {
this.name = "ProformaCannotBeDeletedError";
}
}
export const isProformaCannotBeDeletedError = (e: unknown): e is ProformaCannotBeDeletedError =>
e instanceof ProformaCannotBeDeletedError;

View File

@ -6,6 +6,7 @@ import type { ICompanyPublicServices } from "../../../../../../companies/src/api
import {
type ChangeStatusProformaUseCase,
type CreateProformaUseCase,
type DeleteProformaByIdUseCase,
type GetProformaByIdUseCase,
type IIssuedInvoicePublicServices,
type IssueProformaUseCase,
@ -15,12 +16,14 @@ import {
type UpdateProformaByIdUseCase,
buildChangeStatusProformaUseCase,
buildCreateProformaUseCase,
buildDeleteProformaByIdUseCase,
buildGetProformaByIdUseCase,
buildIssueProformaUseCase,
buildListProformasUseCase,
buildPreviewProformaReportUseCase,
buildProformaCatalogResolvers,
buildProformaCreator,
buildProformaDeleter,
buildProformaFinder,
buildProformaInputMappers,
buildProformaIssueReadModelAssembler,
@ -53,11 +56,7 @@ export type ProformasInternalDeps = {
}) => IssueProformaUseCase;
updateProforma: () => UpdateProformaByIdUseCase;
changeStatusProforma: () => ChangeStatusProformaUseCase;
/*
deleteProforma: () => DeleteProformaUseCase;
*/
deleteProforma: () => DeleteProformaByIdUseCase;
};
};
@ -120,6 +119,9 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
paymentResolver: catalogResolvers.paymentResolver,
targetInvoiceSeriesValidator,
});
const deleter = buildProformaDeleter({
repository,
});
const statusChanger = buildProformaStatusChanger({
repository,
@ -177,6 +179,12 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
transactionManager,
}),
deleteProforma: () =>
buildDeleteProformaByIdUseCase({
deleter,
transactionManager,
}),
reportProformaPdf: () =>
buildReportProformaPdfUseCase({
finder,

View File

@ -5,11 +5,11 @@ import {
requireCompanyContextGuard,
} from "@erp/core/api";
import type { DeleteProformaUseCase } from "../../../../application/index.ts";
import type { DeleteProformaByIdUseCase } from "../../../../application/index.ts";
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
export class DeleteProformaController extends ExpressController {
public constructor(private readonly useCase: DeleteProformaUseCase) {
public constructor(private readonly useCase: DeleteProformaByIdUseCase) {
super();
this.errorMapper = proformasApiErrorMapper;
@ -35,7 +35,7 @@ export class DeleteProformaController extends ExpressController {
const result = await this.useCase.execute({ proforma_id, companyId });
return result.match(
(data) => this.ok(data),
() => this.noContent(),
(err) => this.handleError(err)
);
}

View File

@ -1,6 +1,6 @@
//export * from "./change-status-proforma.controller";
//export * from "./create-proforma.controller";
//export * from "./delete-proforma.controller";
export * from "./delete-proforma.controller";
export * from "./get-proforma-by-id.controller";
export * from "./issue-proforma.controller";
export * from "./list-proformas.controller";

View File

@ -90,8 +90,8 @@ const proformaCannotBeDeletedRule: ErrorToApiRule = {
priority: 120,
matches: (e) => isProformaCannotBeDeletedError(e),
build: (e) =>
new ValidationApiError(
(e as ProformaCannotBeConvertedToInvoiceError).message || "Proforma cannot be deleted."
new ConflictApiError(
(e as Error).message || "Proforma cannot be deleted."
),
};

View File

@ -3,6 +3,7 @@ import { requireIdentityTenant } from "@erp/identity/api";
import { type NextFunction, type Request, type Response, Router } from "express";
import {
DeleteProformaController,
GetProformaByIdController,
IssueProformaController,
ListProformasController,
@ -15,6 +16,7 @@ import {
ChangeStatusProformaByIdParamsRequestSchema,
ChangeStatusProformaByIdRequestSchema,
CreateProformaRequestSchema,
DeleteProformaByIdParamsRequestSchema,
GetProformaByIdRequestSchema,
IssueProformaByIdParamsRequestSchema,
LegacyIssueProformaByIdParamsRequestSchema,
@ -117,17 +119,15 @@ export const proformasRouter = (params: StartParams) => {
}
);
/*router.delete(
router.delete(
"/:proforma_id",
//checkTabContext,
validateRequest(DeleteProformaByIdParamsRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.delete_proforma();
const useCase = deps.useCases.deleteProforma();
const controller = new DeleteProformaController(useCase);
return controller.execute(req, res, next);
}
);*/
);
router.patch(
"/:proforma_id/status",

View File

@ -140,6 +140,26 @@ export class SequelizeProformaRepositoryV2
}
}
async existsIssuedInvoiceBySourceProformaIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
): Promise<Result<boolean, Error>> {
try {
const count = await IssuedInvoiceModel.count({
where: {
company_id: companyId.toString(),
source_proforma_id: id.toString(),
},
transaction,
});
return Result.ok(count > 0);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
async updateStatusByIdInCompany(
companyId: UniqueID,
id: UniqueID,

View File

@ -31,6 +31,7 @@ import {
CustomerInvoiceItemModel,
CustomerInvoiceModel,
CustomerInvoiceTaxModel,
IssuedInvoiceModel,
} from "../../../../common";
import type { SequelizeProformaDomainMapper, SequelizeProformaSummaryMapper } from "../mappers";
@ -214,6 +215,26 @@ export class ProformaRepository
}
}
async existsIssuedInvoiceBySourceProformaIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
): Promise<Result<boolean, Error>> {
try {
const count = await IssuedInvoiceModel.count({
where: {
company_id: companyId.toString(),
source_proforma_id: id.toString(),
},
transaction,
});
return Result.ok(count > 0);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
/**
*
* Actualiza el "status" de una proforma

View File

@ -7,7 +7,7 @@ import { z } from "zod/v4";
*/
export const DeleteProformaByIdParamsRequestSchema = z.object({
proforma_id: z.string(),
proforma_id: z.uuid(),
});
export type DeleteProformaByIdRequestDTO = z.infer<typeof DeleteProformaByIdParamsRequestSchema>;

View File

@ -110,16 +110,23 @@
"unknown_error": "An unexpected error occurred"
},
"delete_proforma_dialog": {
"single_title": "Delete proforma {{reference}}",
"single_description": "Are you sure you want to delete this proforma? This action cannot be undone.",
"second_confirm_single_title": "Additional confirmation",
"second_confirm_single_description": "You are about to delete the proforma. This action cannot be undone. Are you sure you want to continue?",
"multiple_description": "Are you sure you want to delete the {{total}} selected proformas? This action cannot be undone.",
"single_title": "Delete proforma",
"single_description": "This proforma will be deleted. Its reference will not be reused.",
"multiple_title": "Delete proformas",
"multiple_description": "{{count}} draft proformas will be deleted. Their references will not be reused.",
"warning": "This action does not delete issued invoices and does not reuse numbering.",
"confirm_delete": "Confirm deletion",
"deleting": "Deleting...",
"cancel": "Cancel",
"continue": "Delete",
"list_item": "Proforma {{reference}}"
"list_item": "Proforma {{reference}}",
"success_title": "Proforma deleted",
"success_single_message": "Proforma {{reference}} has been deleted successfully.",
"success_multiple_message": "{{count}} proformas have been deleted successfully.",
"error_title": "Unable to delete the proforma",
"error_single_message": "The selected proforma could not be deleted.",
"error_multiple_message": "{{count}} proformas could not be deleted.",
"error_conflict_message": "Only draft proformas can be deleted.",
"error_not_found_message": "The proforma no longer exists or is not available."
},
"change_proforma_status_dialog": {
"title": "Change proforma status",
@ -190,6 +197,9 @@
"title": "Delete customer proforma",
"description": "Delete the selected customer proforma"
},
"delete.error_title": "Unable to delete the proforma",
"delete.error_unexpected_message": "An unexpected error occurred while deleting the proforma."
,
"view": {
"title": "View customer proforma",
"description": "View the details of the selected customer proforma"

View File

@ -111,16 +111,23 @@
"cancel": "Cancelar"
},
"delete_proforma_dialog": {
"single_title": "Eliminar proforma {{reference}}",
"single_description": "¿Seguro que deseas eliminar esta proforma? Esta acción no se puede deshacer.",
"second_confirm_single_title": "Confirmación adicional",
"second_confirm_single_description": "Estás a punto de borrar la proforma. Esta acción masiva no se puede deshacer. ¿Seguro que quieres continuar?",
"multiple_description": "¿Seguro que deseas eliminar las {{total}} proformas seleccionadas? Esta acción no se puede deshacer.",
"single_title": "Eliminar proforma",
"single_description": "Se eliminará la proforma. Su referencia no se reutilizará.",
"multiple_title": "Eliminar proformas",
"multiple_description": "Se eliminarán {{count}} proformas draft. Sus referencias no se reutilizarán.",
"warning": "Esta acción no elimina facturas emitidas ni reutiliza numeración.",
"confirm_delete": "Confirmar eliminación",
"deleting": "Eliminando...",
"cancel": "Cancelar",
"continue": "Eliminar",
"list_item": "Proforma {{reference}}"
"list_item": "Proforma {{reference}}",
"success_title": "Proforma eliminada",
"success_single_message": "La proforma {{reference}} se ha eliminado correctamente.",
"success_multiple_message": "Se han eliminado {{count}} proformas correctamente.",
"error_title": "No se pudo eliminar la proforma",
"error_single_message": "La proforma seleccionada no se ha podido eliminar.",
"error_multiple_message": "{{count}} proformas no se han podido eliminar.",
"error_conflict_message": "Solo se pueden eliminar proformas en borrador.",
"error_not_found_message": "La proforma ya no existe o no está disponible."
},
"change_proforma_status_dialog": {
"title": "Cambiar estado de la proforma",
@ -191,6 +198,9 @@
"title": "Eliminar proforma",
"description": "Eliminar la proforma seleccionada"
},
"delete.error_title": "No se pudo eliminar la proforma",
"delete.error_unexpected_message": "Ha ocurrido un error inesperado al eliminar la proforma."
,
"view": {
"title": "Ver proforma",
"description": "Ver los detalles de la proforma seleccionada"

View File

@ -6,23 +6,20 @@ import { useTranslation } from "../../../i18n";
import type { DeleteProformaByIdParams } from "../../shared";
import { useProformaDeleteMutation } from "../../shared";
import type { DeleteProformaTarget } from "../entities";
type ConfirmStep = "initial" | "second";
type DeleteErrorWithStatus = Error & { status?: number };
interface DeleteProformaDialogState {
open: boolean;
targets: DeleteProformaTarget[];
confirmStep: ConfirmStep;
}
const INITIAL_STATE: DeleteProformaDialogState = {
open: false,
targets: [],
confirmStep: "initial",
};
const buildDeleteParams = (target: DeleteProformaTarget): DeleteProformaByIdParams => ({
id: target.id,
proformaId: target.id,
});
const getProformaLabel = (target: DeleteProformaTarget) => target.reference || `#${target.id}`;
@ -39,7 +36,6 @@ export const useDeleteProformaDialogController = () => {
setState({
open: true,
targets,
confirmStep: "initial",
});
}, []);
@ -48,13 +44,6 @@ export const useDeleteProformaDialogController = () => {
setState(INITIAL_STATE);
}, [deleteMutation.isPending]);
const moveToSecondConfirmStep = React.useCallback(() => {
setState((current) => ({
...current,
confirmStep: "second",
}));
}, []);
const notifySuccess = React.useCallback(
(targets: DeleteProformaTarget[]) => {
if (targets.length === 1) {
@ -77,12 +66,38 @@ export const useDeleteProformaDialogController = () => {
[t]
);
const getDeleteErrorMessage = React.useCallback(
(error: unknown) => {
const status = (error as DeleteErrorWithStatus | undefined)?.status;
if (status === 404) {
return t(
"proformas.delete_proforma_dialog.error_not_found_message",
"La proforma ya no existe o no está disponible."
);
}
if (status === 409) {
return t(
"proformas.delete_proforma_dialog.error_conflict_message",
"Solo se pueden eliminar proformas en borrador."
);
}
return t(
"proformas.delete.error_unexpected_message",
"Ha ocurrido un error inesperado al eliminar la proforma."
);
},
[t]
);
const notifyPartialError = React.useCallback(
(targets: DeleteProformaTarget[], errorCount: number) => {
(targets: DeleteProformaTarget[], errorCount: number, firstError?: unknown) => {
if (targets.length === 1) {
showErrorToast(
t("proformas.delete_proforma_dialog.error_title"),
t("proformas.delete_proforma_dialog.error_single_message")
getDeleteErrorMessage(firstError)
);
return;
}
@ -94,7 +109,7 @@ export const useDeleteProformaDialogController = () => {
})
);
},
[t]
[getDeleteErrorMessage, t]
);
const submitDelete = React.useCallback(async () => {
@ -106,6 +121,9 @@ export const useDeleteProformaDialogController = () => {
const successCount = results.filter((result) => result.status === "fulfilled").length;
const errorCount = results.length - successCount;
const firstRejectedResult = results.find((result) => result.status === "rejected");
const firstError =
firstRejectedResult?.status === "rejected" ? firstRejectedResult.reason : undefined;
if (successCount > 0) {
const fulfilledTargets = targets.filter((_, index) => results[index]?.status === "fulfilled");
@ -116,13 +134,13 @@ export const useDeleteProformaDialogController = () => {
}
if (rejectedTargets.length > 0) {
notifyPartialError(targets, rejectedTargets.length);
notifyPartialError(targets, rejectedTargets.length, firstError);
return;
}
}
if (errorCount > 0) {
notifyPartialError(targets, errorCount);
notifyPartialError(targets, errorCount, firstError);
return;
}
@ -134,11 +152,6 @@ export const useDeleteProformaDialogController = () => {
return;
}
if (state.confirmStep === "initial") {
moveToSecondConfirmStep();
return;
}
try {
await submitDelete();
} catch {
@ -147,13 +160,12 @@ export const useDeleteProformaDialogController = () => {
t("proformas.delete.error_unexpected_message")
);
}
}, [deleteMutation.isPending, moveToSecondConfirmStep, state, submitDelete, t]);
}, [deleteMutation.isPending, state, submitDelete, t]);
return {
open: state.open,
targets: state.targets,
isSubmitting: deleteMutation.isPending,
isSecondConfirmStep: state.confirmStep === "second",
isBulkDelete: state.targets.length > 1,
openDialog,

View File

@ -18,7 +18,6 @@ interface DeleteProformaDialogProps {
targets: DeleteProformaTarget[];
isSubmitting: boolean;
onConfirm: () => void;
isSecondConfirmStep: boolean;
}
const getTargetLabel = (target: DeleteProformaTarget) => target.reference || `#${target.id}`;
@ -29,7 +28,6 @@ export const DeleteProformaDialog = ({
targets,
isSubmitting,
onConfirm,
isSecondConfirmStep,
}: DeleteProformaDialogProps) => {
const { t } = useTranslation();
@ -37,15 +35,7 @@ export const DeleteProformaDialog = ({
const isSingle = total === 1;
const firstTarget = targets[0];
const title = isSecondConfirmStep
? isSingle
? t("proformas.delete_proforma_dialog.second_confirm_single_title", {
reference: getTargetLabel(firstTarget),
})
: t("proformas.delete_proforma_dialog.second_confirm_multiple_title", {
count: total,
})
: isSingle
const title = isSingle
? t("proformas.delete_proforma_dialog.single_title", {
reference: getTargetLabel(firstTarget),
})
@ -53,13 +43,7 @@ export const DeleteProformaDialog = ({
count: total,
});
const description = isSecondConfirmStep
? isSingle
? t("proformas.delete_proforma_dialog.second_confirm_single_description")
: t("proformas.delete_proforma_dialog.second_confirm_multiple_description", {
count: total,
})
: isSingle
const description = isSingle
? t("proformas.delete_proforma_dialog.single_description")
: t("proformas.delete_proforma_dialog.multiple_description", {
count: total,
@ -79,7 +63,11 @@ export const DeleteProformaDialog = ({
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
{!isSecondConfirmStep && total > 1 ? (
<p className="text-sm text-muted-foreground">
{t("proformas.delete_proforma_dialog.warning")}
</p>
{total > 1 ? (
<div className="mt-4 max-h-48 overflow-y-auto rounded-md border p-3 text-sm">
<ul className="space-y-1">
{targets.map((target) => (
@ -104,10 +92,8 @@ export const DeleteProformaDialog = ({
<Spinner className="mr-2 size-4" />
{t("proformas.delete_proforma_dialog.deleting")}
</>
) : isSecondConfirmStep ? (
t("proformas.delete_proforma_dialog.confirm_delete")
) : (
t("proformas.delete_proforma_dialog.continue")
t("proformas.delete_proforma_dialog.confirm_delete")
)}
</Button>
</AlertDialogFooter>

View File

@ -275,6 +275,7 @@ export function useProformasGridColumns(
cell: ({ row }) => {
const proforma = row.original;
const isDraft = proforma.status === PROFORMA_STATUS.DRAFT;
const isIssued = proforma.status === PROFORMA_STATUS.ISSUED;
const isApproved = proforma.status === PROFORMA_STATUS.APPROVED;
const availableTransitions =
@ -396,7 +397,7 @@ export function useProformasGridColumns(
</TooltipProvider>
{/* Eliminar */}
{!isIssued && actionHandlers.onDeleteClick && (
{isDraft && actionHandlers.onDeleteClick && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger

View File

@ -332,7 +332,6 @@ export const ListProformasPage = () => {
{/* Eliminar */}
<DeleteProformaDialog
isSecondConfirmStep={deleteDialogCtrl.isSecondConfirmStep}
isSubmitting={deleteDialogCtrl.isSubmitting}
onConfirm={deleteDialogCtrl.confirmDelete}
onOpenChange={(open) => {

View File

@ -10,7 +10,7 @@ import type { IDataSource } from "@erp/core/client";
*/
export interface DeleteProformaByIdParams {
id: string;
proformaId: string;
signal?: AbortSignal;
}
@ -20,9 +20,9 @@ export const deleteProformaById = (
dataSource: IDataSource,
params: DeleteProformaByIdParams
): Promise<DeleteProformaByIdResult> => {
const { id, signal } = params;
const { proformaId, signal } = params;
if (!id) throw new Error("proformaId is required");
if (!proformaId) throw new Error("proformaId is required");
return dataSource.deleteOne("proformas", id, { signal });
return dataSource.deleteOne("proformas", proformaId, { signal });
};

View File

@ -18,27 +18,32 @@ export const useProformaDeleteMutation = () => {
const queryClient = useQueryClient();
const dataSource = useDataSource();
return useMutation<{ id: string }, DefaultError, DeleteProformaByIdParams, DeleteProformaContext>(
return useMutation<
{ proformaId: string },
DefaultError,
DeleteProformaByIdParams,
DeleteProformaContext
>(
{
mutationKey: DELETE_PROFORMA_MUTATION_KEY,
mutationFn: async ({ id, signal }) => {
if (!id) {
mutationFn: async ({ proformaId, signal }) => {
if (!proformaId) {
throw new Error("proformaId is required");
}
await deleteProformaById(dataSource, { id, signal });
return { id };
await deleteProformaById(dataSource, { proformaId, signal });
return { proformaId };
},
onMutate: async ({ id }) => {
return prepareDeleteProformaOptimisticUpdate(queryClient, id);
onMutate: async ({ proformaId }) => {
return prepareDeleteProformaOptimisticUpdate(queryClient, proformaId);
},
onError: (_error, _params, context) => {
rollbackDeleteProformaOptimisticUpdate(queryClient, context);
},
onSettled: async (_data, _error, { id }) => {
await finalizeDeletedProformaCaches(queryClient, id);
onSettled: async (_data, _error, { proformaId }) => {
await finalizeDeletedProformaCaches(queryClient, proformaId);
},
}
);