feat: eliminar actividad, bloqueado si tiene tareas pendientes

No existia forma de borrar una actividad. Agregado DELETE
/api/activities/:id con boton "Eliminar" en el listado (popup de
confirmacion). Si tiene tareas, se bloquea con 400 y mensaje claro en
vez de arrastrar todo en cascada sin avisar.

De paso: la logica de borrar adjunto (fila + archivo en disco) se
extrajo a un attachments/service.js reutilizable, usado tanto por el
borrado individual como por el borrado en cascada de una actividad
(evita archivos huerfanos). Comentarios de la actividad tambien se
limpian (sin FK fisica cruzada, entity_type/entity_id es polimorfico).

Probado en dev con curl (bloquea con tarea, permite sin tareas, 404
tras borrar) y Playwright (mensaje de advertencia visible).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-15 12:30:53 -05:00
parent 5d48cc6d32
commit 5e0bc0809e
14 changed files with 110 additions and 10 deletions
@@ -1,8 +1,9 @@
import { useState } from 'react';
import { Plus, Save } from 'lucide-react';
import { Plus, Save, Trash2 } from 'lucide-react';
import { useUi } from '../../context/ui-context.jsx';
import { useAuth } from '../../context/auth-context.jsx';
import { useAsync, useAction } from '../../hooks/useAsync.js';
import { listActivitiesByProject, createActivity, getActivity } from '../../services/activities.js';
import { listActivitiesByProject, createActivity, getActivity, deleteActivity } from '../../services/activities.js';
import { listCatalog } from '../../services/catalogs.js';
import { listUsers } from '../../services/users.js';
import { ListView } from '../../components/ListView.jsx';
@@ -18,7 +19,8 @@ import ActivityDetailPanel from './ActivityDetailPanel.jsx';
const EMPTY_FORM = { name: '', description: '', responsibleUserId: '', priorityId: '', statusId: '', startDate: '', dueDate: '', isMilestone: false };
export default function ProjectActivitiesTab({ projectId }) {
const { t, pushToast } = useUi();
const { t, pushToast, openDialog } = useUi();
const { user: currentUser } = useAuth();
const runAction = useAction();
const { data: activities, reload } = useAsync(() => listActivitiesByProject(projectId), [projectId]);
const { data: priorities } = useAsync(() => listCatalog('priorities'), []);
@@ -55,6 +57,25 @@ export default function ProjectActivitiesTab({ projectId }) {
if (selected) setSelected(await getActivity(selected.id));
}
function confirmDelete(row) {
openDialog({
tone: 'warning',
title: t('common_confirm_delete_title'),
message: t('common_confirm_delete_msg'),
actions: [
{ label: t('common_cancel'), variant: 'standard' },
{
label: t('common_delete'), variant: 'danger',
onClick: () => runAction(async () => {
await deleteActivity(row.id);
await reload();
pushToast({ tone: 'success', message: t('common_saved') });
}),
},
],
});
}
const columns = [
{ key: 'code', label: t('activity_code') },
{ key: 'name', label: t('common_name') },
@@ -86,6 +107,9 @@ export default function ProjectActivitiesTab({ projectId }) {
getSearchHaystack={(a) => `${a.code} ${a.name} ${a.responsible_name || ''}`}
getRowKey={(a) => a.id}
onRowClick={openActivity}
rowActions={currentUser?.roleCode !== 'viewer' ? (a) => [
{ label: t('common_delete'), icon: Trash2, danger: true, onClick: () => confirmDelete(a) },
] : undefined}
/>
<DetailPopup open={creating} title={t('activity_new')} onClose={() => setCreating(false)} width={640} height={560}>
+1
View File
@@ -4,5 +4,6 @@ export const listActivitiesByProject = (projectId) => api.get(`/projects/${proje
export const createActivity = (projectId, data) => api.post(`/projects/${projectId}/activities`, data);
export const getActivity = (id) => api.get(`/activities/${id}`);
export const updateActivity = (id, data) => api.put(`/activities/${id}`, data);
export const deleteActivity = (id) => api.delete(`/activities/${id}`);
export const addDependency = (id, dependsOnActivityId) => api.post(`/activities/${id}/dependencies`, { dependsOnActivityId });
export const removeDependency = (id, dependsOnId) => api.delete(`/activities/${id}/dependencies/${dependsOnId}`);