Añadir funcionalidad para eliminar procesos y mejorar la gestión de procesos en la aplicación

This commit is contained in:
David Arranz 2026-10-09 23:11:31 +02:00
parent a622eacfe7
commit 564b666fcc
12 changed files with 63 additions and 11 deletions

View File

@ -19,6 +19,7 @@ export interface IProcessRepository {
action: (context: ProcessCatalogContext) => Result<ProcessTemplate, ProcessError>
): Promise<Result<{ template: ProcessTemplate; context: ProcessCatalogContext }, ProcessError>>;
get(company: UniqueID): Promise<Result<ProcessContext, ProcessError>>;
remove(company: UniqueID, process: UniqueID): Promise<Result<void, ProcessError>>;
save(
company: UniqueID,
action: (context: ProcessContext) => Result<PersonProcess, ProcessError>

View File

@ -105,6 +105,12 @@ export class ProcessUseCases {
: values;
});
}
remove(companyId: string, id: string) {
const company = UniqueID.create(companyId);
const process = UniqueID.create(id);
if (!(company.isSuccess && process.isSuccess)) return Promise.resolve(Result.fail(new ProcessError("INVALID_REQUEST")));
return this.repository.remove(company.data, process.data);
}
update(companyId: string, id: string, input: UpdateProcessInput, actorId: string) {
const company = UniqueID.create(companyId);
const actor = UniqueID.create(actorId);

View File

@ -146,6 +146,15 @@ export class SequelizeProcessRepository implements IProcessRepository {
return Result.ok({ template: candidate.data, context });
});
}
remove(company: UniqueID, process: UniqueID) {
return this.database.transaction(async (transaction) => {
const rows = await this.database.query<{ id: string }>("SELECT id FROM person_processes WHERE id=:id AND companyId=:companyId FOR UPDATE", { replacements: { id: process.toPrimitive(), companyId: company.toPrimitive() }, transaction, type: QueryTypes.SELECT });
if (!rows.length) return Result.fail(new ProcessError("PROCESS_NOT_FOUND"));
await this.database.query("DELETE FROM person_process_revisions WHERE itemId=:id", { replacements: { id: process.toPrimitive() }, transaction });
await this.database.query("DELETE FROM person_processes WHERE id=:id AND companyId=:companyId", { replacements: { id: process.toPrimitive(), companyId: company.toPrimitive() }, transaction });
return Result.ok(undefined);
});
}
save(company: UniqueID, action: Parameters<IProcessRepository["save"]>[1]) {
return this.database.transaction(async (transaction) => {
const loaded = await this.load(company, transaction, true);

View File

@ -327,7 +327,9 @@
"INTERNAL_ERROR": "The operation could not be completed. Try again.",
"FORBIDDEN": "Sign in again.",
"UNAUTHENTICATED": "Sign in again."
}
},
"delete": "Delete process",
"deleteConfirm": "Do you want to delete the process “{{name}}”? This also deletes its history."
},
"remote": {
"title": "Remote access",

View File

@ -327,7 +327,9 @@
"INTERNAL_ERROR": "No se ha podido completar la operación. Inténtalo de nuevo.",
"FORBIDDEN": "Vuelve a iniciar sesión.",
"UNAUTHENTICATED": "Vuelve a iniciar sesión."
}
},
"delete": "Eliminar proceso",
"deleteConfirm": "¿Quieres eliminar el proceso «{{name}}»? Esta acción también elimina su historial."
},
"remote": {
"title": "Accesos remotos",

View File

@ -4,4 +4,6 @@ export type ProcessDetailProps = {
process: PersonProcessDTO;
person: string;
csrfToken: string;
onDelete?: (id: string) => void;
onDeleted?: () => void;
};

View File

@ -1,4 +1,5 @@
import { Button } from "@repo/shadcn-ui/components/button";
import { useState } from "react";
import { Card, CardContent } from "@repo/shadcn-ui/components/card";
import { useTranslation } from "react-i18next";
@ -9,9 +10,10 @@ import { useProcessDetailController } from "../controllers";
import type { ProcessDetailProps } from "../types";
export function ProcessDetail(props: ProcessDetailProps) {
const { process, person, csrfToken } = props;
const { process, person, csrfToken, onDelete, onDeleted } = props;
const { edit, setEdit, refresh, open, resolved } = useProcessDetailController(props);
const { t, i18n } = useTranslation("companies");
const [deleting, setDeleting] = useState(false);
return (
<div className="space-y-5">
<header className="flex flex-wrap items-start justify-between gap-3">
@ -39,6 +41,18 @@ export function ProcessDetail(props: ProcessDetailProps) {
<Button disabled={refresh.isPending} onClick={() => setEdit(true)}>
{t("processes.update")}
</Button>
<Button
disabled={deleting}
variant="destructive"
onClick={() => {
if (!window.confirm(t("processes.deleteConfirm", { name: process.values.name }))) return;
setDeleting(true);
onDelete?.(process.id);
onDeleted?.();
}}
>
{t("processes.delete")}
</Button>
</div>
)}
</header>

View File

@ -4,3 +4,7 @@ import { processRequest } from "../../shared/api/process-request";
export async function getProcesses(company: string) {
return ProcessesResponseSchema.parse(await processRequest(company));
}
export async function deleteProcess(company: string, id: string, token: string) {
await processRequest(`${company}/${id}/delete`, {}, token);
}

View File

@ -1 +1 @@
export { getProcesses } from "./get-processes.api";
export { deleteProcess, getProcesses } from "./get-processes.api";

View File

@ -1,14 +1,15 @@
import { useBrowserSearchParams, useEntityNavigation } from "@metis/core/client";
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { processKeys } from "../../shared/queries";
import { getProcesses } from "../api";
import { deleteProcess, getProcesses } from "../api";
import type { ProcessesPageProps } from "../types";
export function useProcessesPageController({ company }: ProcessesPageProps) {
const { t } = useTranslation("companies");
const client = useQueryClient();
const [create, setCreate] = useState(false);
const { selectedId: selected, selectEntity: setSelected } = useEntityNavigation(
`/companies/${company.id}/processes`
@ -32,6 +33,7 @@ export function useProcessesPageController({ company }: ProcessesPageProps) {
params.delete("closed");
}
});
const remove = useMutation({ mutationFn: ({ id, token }: { id: string; token: string }) => deleteProcess(company.id, id, token), onSuccess: async () => { await client.invalidateQueries({ queryKey: processKeys.company(company.id) }); setSelected(undefined); } });
const query = useQuery({
queryKey: processKeys.company(company.id),
queryFn: () => getProcesses(company.id),
@ -56,6 +58,7 @@ export function useProcessesPageController({ company }: ProcessesPageProps) {
closed,
setClosed,
query,
remove,
person,
current,
missingView: Boolean(selected && query.isSuccess && !current),

View File

@ -28,6 +28,7 @@ export function ProcessesPage(props: ProcessesPageProps) {
closed,
setClosed,
query,
remove,
person,
current,
missingView,
@ -66,6 +67,8 @@ export function ProcessesPage(props: ProcessesPageProps) {
csrfToken={csrfToken}
person={person(current.values.personId)}
process={current}
onDeleted={() => setSelected(undefined)}
onDelete={(id) => remove.mutate({ id, token: csrfToken })}
/>
) : (
!selected && (
@ -150,10 +153,16 @@ export function ProcessesPage(props: ProcessesPageProps) {
hideable: false,
render: (p) => (
<EntityRowActions label={t("actions")}>
{" "}
<EntityAction onClick={() => setSelected(p.id)}>
{t("processes.openDetail")}
</EntityAction>{" "}
<EntityAction onClick={() => setSelected(p.id)}>{t("processes.openDetail")}</EntityAction>
<EntityAction
onClick={() => {
if (window.confirm(t("processes.deleteConfirm", { name: p.values.name }))) {
remove.mutate({ id: p.id, token: csrfToken });
}
}}
>
{t("processes.delete")}
</EntityAction>
</EntityRowActions>
),
},

View File

@ -10,7 +10,7 @@ export async function processRequest(path: string, body?: unknown, token?: strin
body === undefined ? {} : { "content-type": "application/json", "x-csrf-token": token ?? "" },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
const data: unknown = await response.json();
const data: unknown = response.status === 204 ? undefined : await response.json();
if (!response.ok) {
const error = z.object({ code: z.string() }).safeParse(data);
throw new ProcessApiError(error.success ? error.data.code : "INTERNAL_ERROR");