feat: eliminar proyecto (logico), papelera para superadmin y orden por creacion

- DELETE /api/projects/🆔 eliminacion logica (deleted_at/deleted_by), mismo
  permiso que editar el proyecto. Popup de confirmacion antes de aplicarla.
- Papelera (GET /api/projects?trash=true, POST /:id/restore, DELETE
  /:id/permanent) restringida al superadmin; el borrado fisico exige que el
  proyecto ya este en la papelera y arrastra actividades/comentarios/adjuntos
  por ON DELETE CASCADE.
- Dashboard (KPIs, actividades pendientes/calendario, proyectos por estado)
  excluye proyectos eliminados logicamente.
- Columna "Creado" en el listado de proyectos, ordenado por defecto de mas
  nuevo a mas antiguo (ListView admite initialSort ademas del orden por clic
  en cabecera que ya tenia).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-09 09:26:35 -05:00
parent dd7caf51c7
commit 5a25ed066d
16 changed files with 247 additions and 33 deletions
+19 -6
View File
@@ -8,7 +8,7 @@ const LIST_SELECT = `
p.requesting_department_id, rd.name AS requesting_department_name,
p.responsible_department_id, sd.name AS responsible_department_name,
p.start_date, p.estimated_end_date, p.actual_end_date, p.progress_pct,
p.risk_level, p.notes, p.created_by, p.created_at, p.updated_at
p.risk_level, p.notes, p.created_by, p.created_at, p.updated_at, p.deleted_at, p.deleted_by
FROM projects p
JOIN priorities pr ON pr.id = p.priority_id
JOIN project_statuses ps ON ps.id = p.status_id
@@ -18,22 +18,35 @@ const LIST_SELECT = `
LEFT JOIN departments sd ON sd.id = p.responsible_department_id
`;
export async function findAll({ statusId, priorityId, ownerUserId } = {}) {
const clauses = [];
export async function findAll({ statusId, priorityId, ownerUserId, trash } = {}) {
const clauses = [trash ? 'p.deleted_at IS NOT NULL' : 'p.deleted_at IS NULL'];
const params = [];
if (statusId) { clauses.push('p.status_id = ?'); params.push(statusId); }
if (priorityId) { clauses.push('p.priority_id = ?'); params.push(priorityId); }
if (ownerUserId) { clauses.push('p.owner_user_id = ?'); params.push(ownerUserId); }
const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
const where = `WHERE ${clauses.join(' AND ')}`;
const [rows] = await pool.query(`${LIST_SELECT} ${where} ORDER BY p.created_at DESC`, params);
return rows;
}
export async function findById(id) {
const [rows] = await pool.query(`${LIST_SELECT} WHERE p.id = ?`, [id]);
export async function findById(id, { includeDeleted = false } = {}) {
const where = includeDeleted ? 'p.id = ?' : 'p.id = ? AND p.deleted_at IS NULL';
const [rows] = await pool.query(`${LIST_SELECT} WHERE ${where}`, [id]);
return rows[0] || null;
}
export async function softDelete(id, userId) {
await pool.query('UPDATE projects SET deleted_at = NOW(), deleted_by = ? WHERE id = ? AND deleted_at IS NULL', [userId, id]);
}
export async function restore(id) {
await pool.query('UPDATE projects SET deleted_at = NULL, deleted_by = NULL WHERE id = ?', [id]);
}
export async function hardDelete(id) {
await pool.query('DELETE FROM projects WHERE id = ?', [id]);
}
export async function create(data) {
const [result] = await pool.query(
`INSERT INTO projects