refactor: renombra Subtareas a Tareas en todo el sistema
Actividad -> Tarea, sin nivel "sub" intermedio (no hay una entidad "Tarea" de la que estas fueran sub-, asi que el prefijo sobraba). Tabla subtasks -> tasks (RENAME TABLE idempotente en la migracion 005; 001_initial.sql tambien actualizado para que una instalacion nueva cree la tabla ya con el nombre final, sin pasar por el nombre viejo). Modulo backend, rutas, servicio frontend, pestaña del detalle de actividad, i18n y documentacion actualizados. Sin cambios de comportamiento, solo nombres. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,7 +74,7 @@ Adaptación web (no es una app empaquetada, corre en pestaña de navegador):
|
|||||||
- Seguridad (usuarios, roles fijos, sesión, auditoría)
|
- Seguridad (usuarios, roles fijos, sesión, auditoría)
|
||||||
- Organización (departamentos con sub-áreas, usuarios)
|
- Organización (departamentos con sub-áreas, usuarios)
|
||||||
- Proyectos (con miembros)
|
- Proyectos (con miembros)
|
||||||
- Actividades + Subtareas + dependencias simples
|
- Actividades + Tareas + dependencias simples
|
||||||
- Dashboard (KPIs)
|
- Dashboard (KPIs)
|
||||||
- Calendario y Gantt de solo lectura
|
- Calendario y Gantt de solo lectura
|
||||||
- Comentarios y adjuntos
|
- Comentarios y adjuntos
|
||||||
@@ -140,7 +140,7 @@ Adaptación web (no es una app empaquetada, corre en pestaña de navegador):
|
|||||||
## Flujos criticos
|
## Flujos criticos
|
||||||
|
|
||||||
1. Login → Dashboard con KPIs del usuario.
|
1. Login → Dashboard con KPIs del usuario.
|
||||||
2. Crear proyecto → asignar miembros → crear actividades → crear subtareas → avance se recalcula solo.
|
2. Crear proyecto → asignar miembros → crear actividades → crear tareas → avance se recalcula solo.
|
||||||
3. Actividad vencida o por vencer → notificación in-app al responsable.
|
3. Actividad vencida o por vencer → notificación in-app al responsable.
|
||||||
|
|
||||||
## Entorno operativo
|
## Entorno operativo
|
||||||
|
|||||||
@@ -73,8 +73,8 @@
|
|||||||
- `GET /api/activities/:id` · `PUT /api/activities/:id`
|
- `GET /api/activities/:id` · `PUT /api/activities/:id`
|
||||||
- `POST /api/activities/:id/dependencies` · `DELETE /api/activities/:id/dependencies/:dependsOnId`
|
- `POST /api/activities/:id/dependencies` · `DELETE /api/activities/:id/dependencies/:dependsOnId`
|
||||||
|
|
||||||
### Subtareas (`/api/activities/:activityId/subtasks`)
|
### Tareas (`/api/activities/:activityId/tasks`)
|
||||||
- `GET` · `POST` · `PUT /api/subtasks/:id` · `PATCH /api/subtasks/:id/done` · `DELETE /api/subtasks/:id`
|
- `GET` · `POST` · `PUT /api/tasks/:id` · `PATCH /api/tasks/:id/done` · `DELETE /api/tasks/:id`
|
||||||
|
|
||||||
### Comentarios (`/api/comments`)
|
### Comentarios (`/api/comments`)
|
||||||
- `GET /api/comments?entityType=project&entityId=1` · `POST /api/comments`
|
- `GET /api/comments?entityType=project&entityId=1` · `POST /api/comments`
|
||||||
|
|||||||
+4
-4
@@ -32,7 +32,7 @@
|
|||||||
| `project_members` | usuarios asignados a un proyecto (reemplaza "equipos" como módulo aparte) |
|
| `project_members` | usuarios asignados a un proyecto (reemplaza "equipos" como módulo aparte) |
|
||||||
| `activities` | actividades de un proyecto |
|
| `activities` | actividades de un proyecto |
|
||||||
| `activity_dependencies` | dependencias simples entre actividades ("depende de") |
|
| `activity_dependencies` | dependencias simples entre actividades ("depende de") |
|
||||||
| `subtasks` | checklist liviano dentro de una actividad |
|
| `tasks` | checklist liviano dentro de una actividad ("Tareas" en la UI; renombrada desde `subtasks` en v1.8.0) |
|
||||||
| `comments` | comentarios sobre proyecto o actividad (`entity_type` + `entity_id`) |
|
| `comments` | comentarios sobre proyecto o actividad (`entity_type` + `entity_id`) |
|
||||||
| `attachments` | archivos adjuntos a proyecto o actividad |
|
| `attachments` | archivos adjuntos a proyecto o actividad |
|
||||||
| `notifications` | notificaciones in-app por usuario |
|
| `notifications` | notificaciones in-app por usuario |
|
||||||
@@ -49,15 +49,15 @@
|
|||||||
- `activities.project_id -> projects.id`, `activities.status_id -> activity_statuses.id`,
|
- `activities.project_id -> projects.id`, `activities.status_id -> activity_statuses.id`,
|
||||||
`activities.priority_id -> priorities.id`, `activities.responsible_user_id -> users.id`
|
`activities.priority_id -> priorities.id`, `activities.responsible_user_id -> users.id`
|
||||||
- `activity_dependencies.activity_id / depends_on_activity_id -> activities.id`
|
- `activity_dependencies.activity_id / depends_on_activity_id -> activities.id`
|
||||||
- `subtasks.activity_id -> activities.id`, `subtasks.responsible_user_id -> users.id` (nullable)
|
- `tasks.activity_id -> activities.id`, `tasks.responsible_user_id -> users.id` (nullable)
|
||||||
- `comments.entity_id` / `attachments.entity_id` apuntan a `projects.id` o `activities.id` según
|
- `comments.entity_id` / `attachments.entity_id` apuntan a `projects.id` o `activities.id` según
|
||||||
`entity_type` (sin FK física cruzada — se valida en el servicio)
|
`entity_type` (sin FK física cruzada — se valida en el servicio)
|
||||||
|
|
||||||
## Reglas de datos
|
## Reglas de datos
|
||||||
|
|
||||||
- `projects.progress_pct` y `activities.progress_pct` son **calculados por el servicio**, no
|
- `projects.progress_pct` y `activities.progress_pct` son **calculados por el servicio**, no
|
||||||
editables directo por el cliente: actividad con subtareas = % de subtareas hechas; actividad sin
|
editables directo por el cliente: actividad con tareas = % de tareas hechas; actividad sin
|
||||||
subtareas = manual (0-100); proyecto = promedio del avance de sus actividades.
|
tareas = manual (0-100); proyecto = promedio del avance de sus actividades.
|
||||||
- `users.password_hash` con bcrypt, nunca texto plano ni reversible.
|
- `users.password_hash` con bcrypt, nunca texto plano ni reversible.
|
||||||
- `users.status` (`pending_verification`/`pending_approval`/`active`/`invited`) es el ciclo de vida
|
- `users.status` (`pending_verification`/`pending_approval`/`active`/`invited`) es el ciclo de vida
|
||||||
de la cuenta, independiente de `is_active` (que sigue siendo el interruptor manual del admin
|
de la cuenta, independiente de `is_active` (que sigue siendo el interruptor manual del admin
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# Changelog - project.linkedcorp.net
|
# Changelog - project.linkedcorp.net
|
||||||
|
|
||||||
|
## [1.8.0] - 2026-09-10
|
||||||
|
### Changed
|
||||||
|
- Renombrado "Subtareas" a "Tareas" en todo el sistema (Actividad → Tarea, sin nivel "sub"
|
||||||
|
intermedio): tabla `subtasks` → `tasks` (migración `005`, `RENAME TABLE` idempotente), módulo
|
||||||
|
backend `subtasks/` → `tasks/`, rutas `/api/activities/:id/subtasks` → `/api/activities/:id/tasks`
|
||||||
|
y `/api/subtasks/:id` → `/api/tasks/:id`, servicio frontend, pestaña dentro del detalle de
|
||||||
|
actividad, i18n (es-EC/en-US) y documentación (`API.md`, `BASE_DE_DATOS.md`, `AGENTS.md`,
|
||||||
|
`README.md`, `SPEC.md`). Sin cambios de comportamiento, solo nombres.
|
||||||
|
|
||||||
## [1.7.0] - 2026-09-10
|
## [1.7.0] - 2026-09-10
|
||||||
### Added
|
### Added
|
||||||
- Agregar miembro a un proyecto ahora acepta un **correo que todavía no es usuario del sistema**
|
- Agregar miembro a un proyecto ahora acepta un **correo que todavía no es usuario del sistema**
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ project.linkedcorp.net/
|
|||||||
|
|
||||||
- Seguridad (usuarios, roles, auditoría)
|
- Seguridad (usuarios, roles, auditoría)
|
||||||
- Organización (departamentos, usuarios)
|
- Organización (departamentos, usuarios)
|
||||||
- Proyectos, Actividades y Subtareas
|
- Proyectos, Actividades y Tareas
|
||||||
- Dashboard, Calendario, Gantt (solo lectura)
|
- Dashboard, Calendario, Gantt (solo lectura)
|
||||||
- Comentarios, adjuntos, notificaciones in-app
|
- Comentarios, adjuntos, notificaciones in-app
|
||||||
- Configuración (catálogos)
|
- Configuración (catálogos)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ tiene cada cosa — sin que cargar la información sea tedioso para quien lo usa
|
|||||||
| Administrador | Todo, incluida gestión de usuarios/roles/catálogos y aprobar/rechazar registros |
|
| Administrador | Todo, incluida gestión de usuarios/roles/catálogos y aprobar/rechazar registros |
|
||||||
| Jefe de TI | Ve y gestiona todos los proyectos/actividades, aprueba; no administra usuarios |
|
| Jefe de TI | Ve y gestiona todos los proyectos/actividades, aprueba; no administra usuarios |
|
||||||
| Coordinador / Líder de proyecto | Crea/edita los proyectos donde es responsable o miembro, gestiona sus actividades |
|
| Coordinador / Líder de proyecto | Crea/edita los proyectos donde es responsable o miembro, gestiona sus actividades |
|
||||||
| Colaborador | Ve y actualiza lo asignado a él (actividades/subtareas propias), comenta |
|
| Colaborador | Ve y actualiza lo asignado a él (actividades/tareas propias), comenta |
|
||||||
| Consultor | Solo lectura en todo |
|
| Consultor | Solo lectura en todo |
|
||||||
|
|
||||||
## Modulos (MVP)
|
## Modulos (MVP)
|
||||||
@@ -54,9 +54,9 @@ tiene cada cosa — sin que cargar la información sea tedioso para quien lo usa
|
|||||||
("depende de" otra actividad del mismo proyecto).
|
("depende de" otra actividad del mismo proyecto).
|
||||||
- Estados: Pendiente, En progreso, En revisión, Bloqueada, En espera, Completada, Cancelada.
|
- Estados: Pendiente, En progreso, En revisión, Bloqueada, En espera, Completada, Cancelada.
|
||||||
|
|
||||||
### 5. Subtareas
|
### 5. Tareas
|
||||||
- Checklist liviano dentro de una actividad: nombre, responsable opcional, hecho/no hecho, fecha
|
- Checklist liviano dentro de una actividad: nombre, responsable opcional, hecho/no hecho, fecha
|
||||||
límite opcional. Si una actividad tiene subtareas, su avance se calcula solo desde ellas.
|
límite opcional. Si una actividad tiene tareas, su avance se calcula solo desde ellas.
|
||||||
|
|
||||||
### 6. Dashboard
|
### 6. Dashboard
|
||||||
- KPIs: proyectos activos, proyectos atrasados, actividades pendientes/vencidas/para hoy, avance
|
- KPIs: proyectos activos, proyectos atrasados, actividades pendientes/vencidas/para hoy, avance
|
||||||
@@ -90,7 +90,7 @@ tiene cada cosa — sin que cargar la información sea tedioso para quien lo usa
|
|||||||
1. Administrador crea departamentos y usuarios (con rol y departamento).
|
1. Administrador crea departamentos y usuarios (con rol y departamento).
|
||||||
2. Jefe de TI o Coordinador crea un proyecto, asigna miembros, define fechas y prioridad.
|
2. Jefe de TI o Coordinador crea un proyecto, asigna miembros, define fechas y prioridad.
|
||||||
3. Se crean actividades dentro del proyecto (con responsable, fechas, dependencias si aplica).
|
3. Se crean actividades dentro del proyecto (con responsable, fechas, dependencias si aplica).
|
||||||
4. Cada responsable actualiza el avance de sus actividades/subtareas; el avance del proyecto se
|
4. Cada responsable actualiza el avance de sus actividades/tareas; el avance del proyecto se
|
||||||
recalcula solo.
|
recalcula solo.
|
||||||
5. El Jefe de TI entra al Dashboard y ve de un vistazo activos/atrasados/pendientes/avance.
|
5. El Jefe de TI entra al Dashboard y ve de un vistazo activos/atrasados/pendientes/avance.
|
||||||
6. Vencimientos se marcan automáticamente (🟢 en tiempo / 🟡 próximo a vencer / 🔴 vencido) y generan
|
6. Vencimientos se marcan automáticamente (🟢 en tiempo / 🟡 próximo a vencer / 🔴 vencido) y generan
|
||||||
@@ -99,7 +99,7 @@ tiene cada cosa — sin que cargar la información sea tedioso para quien lo usa
|
|||||||
## Reglas funcionales importantes
|
## Reglas funcionales importantes
|
||||||
|
|
||||||
- El listado se ve primero; la creación se abre solo con el botón "Nuevo" (regla de UX del grupo).
|
- El listado se ve primero; la creación se abre solo con el botón "Nuevo" (regla de UX del grupo).
|
||||||
- El % de avance de una actividad con subtareas es automático (no editable a mano); sin subtareas,
|
- El % de avance de una actividad con tareas es automático (no editable a mano); sin tareas,
|
||||||
es manual (0-100).
|
es manual (0-100).
|
||||||
- El % de avance de un proyecto es el promedio del avance de sus actividades (no editable a mano).
|
- El % de avance de un proyecto es el promedio del avance de sus actividades (no editable a mano).
|
||||||
- Una actividad no puede marcarse "Completada" si depende de otra que no lo está (aviso, no bloqueo
|
- Una actividad no puede marcarse "Completada" si depende de otra que no lo está (aviso, no bloqueo
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ CREATE TABLE IF NOT EXISTS activity_dependencies (
|
|||||||
CONSTRAINT fk_dep_depends_on FOREIGN KEY (depends_on_activity_id) REFERENCES activities(id) ON DELETE CASCADE
|
CONSTRAINT fk_dep_depends_on FOREIGN KEY (depends_on_activity_id) REFERENCES activities(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS subtasks (
|
CREATE TABLE IF NOT EXISTS tasks (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
activity_id INT NOT NULL,
|
activity_id INT NOT NULL,
|
||||||
name VARCHAR(160) NOT NULL,
|
name VARCHAR(160) NOT NULL,
|
||||||
@@ -141,8 +141,8 @@ CREATE TABLE IF NOT EXISTS subtasks (
|
|||||||
due_date DATE NULL,
|
due_date DATE NULL,
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
CONSTRAINT fk_subtasks_activity FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE CASCADE,
|
CONSTRAINT fk_tasks_activity FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE CASCADE,
|
||||||
CONSTRAINT fk_subtasks_responsible FOREIGN KEY (responsible_user_id) REFERENCES users(id) ON DELETE SET NULL
|
CONSTRAINT fk_tasks_responsible FOREIGN KEY (responsible_user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS comments (
|
CREATE TABLE IF NOT EXISTS comments (
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- Renombra "Subtareas" a "Tareas" (Actividad -> Tarea, sin nivel "sub" intermedio). Idempotente:
|
||||||
|
-- RENAME TABLE no se puede repetir tal cual, se guarda con un chequeo de information_schema.
|
||||||
|
SET @exists := (
|
||||||
|
SELECT COUNT(*) FROM information_schema.tables
|
||||||
|
WHERE table_schema = DATABASE() AND table_name = 'subtasks'
|
||||||
|
);
|
||||||
|
SET @sql := IF(@exists > 0, 'RENAME TABLE subtasks TO tasks', 'SELECT 1');
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-backend",
|
"name": "project-linkedcorp-backend",
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@ import departmentsRoutes from './modules/departments/routes.js';
|
|||||||
import catalogsRoutes from './modules/catalogs/routes.js';
|
import catalogsRoutes from './modules/catalogs/routes.js';
|
||||||
import projectsRoutes from './modules/projects/routes.js';
|
import projectsRoutes from './modules/projects/routes.js';
|
||||||
import activitiesRoutes from './modules/activities/routes.js';
|
import activitiesRoutes from './modules/activities/routes.js';
|
||||||
import subtasksRoutes from './modules/subtasks/routes.js';
|
import tasksRoutes from './modules/tasks/routes.js';
|
||||||
import commentsRoutes from './modules/comments/routes.js';
|
import commentsRoutes from './modules/comments/routes.js';
|
||||||
import attachmentsRoutes from './modules/attachments/routes.js';
|
import attachmentsRoutes from './modules/attachments/routes.js';
|
||||||
import notificationsRoutes from './modules/notifications/routes.js';
|
import notificationsRoutes from './modules/notifications/routes.js';
|
||||||
@@ -41,7 +41,7 @@ app.use('/api/settings', settingsRoutes);
|
|||||||
// Mas generico (mount en /api sin sub-path propio) — SIEMPRE al final de los /api/*, para no
|
// Mas generico (mount en /api sin sub-path propio) — SIEMPRE al final de los /api/*, para no
|
||||||
// interceptar con su requireAuth interno una ruta que en realidad pertenece a un router mas
|
// interceptar con su requireAuth interno una ruta que en realidad pertenece a un router mas
|
||||||
// especifico registrado despues (ver ESTADO.md, deuda tecnica).
|
// especifico registrado despues (ver ESTADO.md, deuda tecnica).
|
||||||
app.use('/api', subtasksRoutes);
|
app.use('/api', tasksRoutes);
|
||||||
|
|
||||||
app.use(notFoundHandler);
|
app.use(notFoundHandler);
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ export async function updateProgress(id, progressPct) {
|
|||||||
await pool.query('UPDATE activities SET progress_pct = ? WHERE id = ?', [progressPct, id]);
|
await pool.query('UPDATE activities SET progress_pct = ? WHERE id = ?', [progressPct, id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countSubtasks(id) {
|
export async function countTasks(id) {
|
||||||
const [rows] = await pool.query('SELECT COUNT(*) AS c FROM subtasks WHERE activity_id = ?', [id]);
|
const [rows] = await pool.query('SELECT COUNT(*) AS c FROM tasks WHERE activity_id = ?', [id]);
|
||||||
return rows[0].c;
|
return rows[0].c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,15 +65,15 @@ export async function updateActivity(id, payload, currentUser) {
|
|||||||
throw new HttpError(400, 'Nombre, prioridad y estado son obligatorios');
|
throw new HttpError(400, 'Nombre, prioridad y estado son obligatorios');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Colaborador no puede reasignar la actividad a otra persona ni cambiar avance manual si hay subtareas.
|
// Colaborador no puede reasignar la actividad a otra persona ni cambiar avance manual si hay tareas.
|
||||||
const nextResponsible = currentUser.roleCode === ROLES.COLLABORATOR
|
const nextResponsible = currentUser.roleCode === ROLES.COLLABORATOR
|
||||||
? activity.responsible_user_id
|
? activity.responsible_user_id
|
||||||
: (payload.responsibleUserId ?? activity.responsible_user_id);
|
: (payload.responsibleUserId ?? activity.responsible_user_id);
|
||||||
|
|
||||||
const subtaskCount = await repo.countSubtasks(id);
|
const taskCount = await repo.countTasks(id);
|
||||||
const updated = await repo.update(id, { ...payload, responsibleUserId: nextResponsible });
|
const updated = await repo.update(id, { ...payload, responsibleUserId: nextResponsible });
|
||||||
|
|
||||||
if (subtaskCount === 0 && payload.progressPct !== undefined) {
|
if (taskCount === 0 && payload.progressPct !== undefined) {
|
||||||
const pct = Math.max(0, Math.min(100, Number(payload.progressPct)));
|
const pct = Math.max(0, Math.min(100, Number(payload.progressPct)));
|
||||||
await repo.updateProgress(id, pct);
|
await repo.updateProgress(id, pct);
|
||||||
updated.progress_pct = pct;
|
updated.progress_pct = pct;
|
||||||
@@ -120,8 +120,8 @@ export async function removeDependency(activityId, dependsOnActivityId, currentU
|
|||||||
return deps;
|
return deps;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalcula el avance de una actividad a partir de sus subtareas (si tiene) y propaga al proyecto.
|
// Recalcula el avance de una actividad a partir de sus tareas (si tiene) y propaga al proyecto.
|
||||||
export async function recalcFromSubtasks(activityId, doneCount, totalCount) {
|
export async function recalcFromTasks(activityId, doneCount, totalCount) {
|
||||||
if (totalCount === 0) return;
|
if (totalCount === 0) return;
|
||||||
const pct = Math.round((doneCount / totalCount) * 10000) / 100;
|
const pct = Math.round((doneCount / totalCount) * 10000) / 100;
|
||||||
await repo.updateProgress(activityId, pct);
|
await repo.updateProgress(activityId, pct);
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ok, asyncHandler } from '../../utils/http.js';
|
|
||||||
import * as service from './service.js';
|
|
||||||
|
|
||||||
export const getSubtasks = asyncHandler(async (req, res) => {
|
|
||||||
ok(res, await service.listByActivity(Number(req.params.activityId)));
|
|
||||||
});
|
|
||||||
|
|
||||||
export const postSubtask = asyncHandler(async (req, res) => {
|
|
||||||
ok(res, await service.createSubtask(Number(req.params.activityId), req.body, req.currentUser), 201);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const putSubtask = asyncHandler(async (req, res) => {
|
|
||||||
ok(res, await service.updateSubtask(Number(req.params.id), req.body, req.currentUser));
|
|
||||||
});
|
|
||||||
|
|
||||||
export const patchSubtaskDone = asyncHandler(async (req, res) => {
|
|
||||||
ok(res, await service.setSubtaskDone(Number(req.params.id), !!req.body.isDone, req.currentUser));
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteSubtask = asyncHandler(async (req, res) => {
|
|
||||||
await service.removeSubtask(Number(req.params.id), req.currentUser);
|
|
||||||
ok(res, { deleted: true });
|
|
||||||
});
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
import { requireAuth } from '../../middleware/auth.js';
|
|
||||||
import { getSubtasks, postSubtask, putSubtask, patchSubtaskDone, deleteSubtask } from './controller.js';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.use(requireAuth);
|
|
||||||
router.get('/activities/:activityId/subtasks', getSubtasks);
|
|
||||||
router.post('/activities/:activityId/subtasks', postSubtask);
|
|
||||||
router.put('/subtasks/:id', putSubtask);
|
|
||||||
router.patch('/subtasks/:id/done', patchSubtaskDone);
|
|
||||||
router.delete('/subtasks/:id', deleteSubtask);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import * as repo from './repository.js';
|
|
||||||
import * as activitiesRepo from '../activities/repository.js';
|
|
||||||
import * as projectsRepo from '../projects/repository.js';
|
|
||||||
import { canWriteActivity, recalcFromSubtasks } from '../activities/service.js';
|
|
||||||
import { HttpError } from '../../utils/http.js';
|
|
||||||
import { logAudit } from '../../utils/audit.js';
|
|
||||||
|
|
||||||
async function requireActivityAccess(activityId, currentUser) {
|
|
||||||
const activity = await activitiesRepo.findById(activityId);
|
|
||||||
if (!activity) throw new HttpError(404, 'Actividad no encontrada');
|
|
||||||
const project = await projectsRepo.findById(activity.project_id);
|
|
||||||
if (!(await canWriteActivity(currentUser, project, activity))) {
|
|
||||||
throw new HttpError(403, 'No tienes permisos sobre esta actividad');
|
|
||||||
}
|
|
||||||
return activity;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const listByActivity = (activityId) => repo.findByActivity(activityId);
|
|
||||||
|
|
||||||
export async function createSubtask(activityId, payload, currentUser) {
|
|
||||||
await requireActivityAccess(activityId, currentUser);
|
|
||||||
if (!payload.name) throw new HttpError(400, 'El nombre es obligatorio');
|
|
||||||
const subtask = await repo.create(activityId, payload);
|
|
||||||
await logAudit({ userId: currentUser.id, action: 'create', entityType: 'subtask', entityId: subtask.id });
|
|
||||||
const counts = await repo.countsByActivity(activityId);
|
|
||||||
await recalcFromSubtasks(activityId, counts.done, counts.total);
|
|
||||||
return subtask;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateSubtask(id, payload, currentUser) {
|
|
||||||
const subtask = await repo.findById(id);
|
|
||||||
if (!subtask) throw new HttpError(404, 'Subtarea no encontrada');
|
|
||||||
await requireActivityAccess(subtask.activity_id, currentUser);
|
|
||||||
if (!payload.name) throw new HttpError(400, 'El nombre es obligatorio');
|
|
||||||
const updated = await repo.update(id, payload);
|
|
||||||
await logAudit({ userId: currentUser.id, action: 'update', entityType: 'subtask', entityId: id });
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setSubtaskDone(id, isDone, currentUser) {
|
|
||||||
const subtask = await repo.findById(id);
|
|
||||||
if (!subtask) throw new HttpError(404, 'Subtarea no encontrada');
|
|
||||||
await requireActivityAccess(subtask.activity_id, currentUser);
|
|
||||||
const updated = await repo.setDone(id, isDone);
|
|
||||||
await logAudit({ userId: currentUser.id, action: isDone ? 'complete' : 'reopen', entityType: 'subtask', entityId: id });
|
|
||||||
const counts = await repo.countsByActivity(subtask.activity_id);
|
|
||||||
await recalcFromSubtasks(subtask.activity_id, counts.done, counts.total);
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeSubtask(id, currentUser) {
|
|
||||||
const subtask = await repo.findById(id);
|
|
||||||
if (!subtask) throw new HttpError(404, 'Subtarea no encontrada');
|
|
||||||
await requireActivityAccess(subtask.activity_id, currentUser);
|
|
||||||
await repo.remove(id);
|
|
||||||
await logAudit({ userId: currentUser.id, action: 'delete', entityType: 'subtask', entityId: id });
|
|
||||||
const counts = await repo.countsByActivity(subtask.activity_id);
|
|
||||||
await recalcFromSubtasks(subtask.activity_id, counts.done, counts.total);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ok, asyncHandler } from '../../utils/http.js';
|
||||||
|
import * as service from './service.js';
|
||||||
|
|
||||||
|
export const getTasks = asyncHandler(async (req, res) => {
|
||||||
|
ok(res, await service.listByActivity(Number(req.params.activityId)));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const postTask = asyncHandler(async (req, res) => {
|
||||||
|
ok(res, await service.createTask(Number(req.params.activityId), req.body, req.currentUser), 201);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const putTask = asyncHandler(async (req, res) => {
|
||||||
|
ok(res, await service.updateTask(Number(req.params.id), req.body, req.currentUser));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const patchTaskDone = asyncHandler(async (req, res) => {
|
||||||
|
ok(res, await service.setTaskDone(Number(req.params.id), !!req.body.isDone, req.currentUser));
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteTask = asyncHandler(async (req, res) => {
|
||||||
|
await service.removeTask(Number(req.params.id), req.currentUser);
|
||||||
|
ok(res, { deleted: true });
|
||||||
|
});
|
||||||
+10
-10
@@ -2,47 +2,47 @@ import { pool } from '../../config/db.js';
|
|||||||
|
|
||||||
export async function findByActivity(activityId) {
|
export async function findByActivity(activityId) {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`SELECT s.id, s.activity_id, s.name, s.responsible_user_id, u.name AS responsible_name,
|
`SELECT t.id, t.activity_id, t.name, t.responsible_user_id, u.name AS responsible_name,
|
||||||
s.is_done, s.due_date, s.created_at
|
t.is_done, t.due_date, t.created_at
|
||||||
FROM subtasks s LEFT JOIN users u ON u.id = s.responsible_user_id
|
FROM tasks t LEFT JOIN users u ON u.id = t.responsible_user_id
|
||||||
WHERE s.activity_id = ? ORDER BY s.created_at`,
|
WHERE t.activity_id = ? ORDER BY t.created_at`,
|
||||||
[activityId]
|
[activityId]
|
||||||
);
|
);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findById(id) {
|
export async function findById(id) {
|
||||||
const [rows] = await pool.query('SELECT * FROM subtasks WHERE id = ?', [id]);
|
const [rows] = await pool.query('SELECT * FROM tasks WHERE id = ?', [id]);
|
||||||
return rows[0] || null;
|
return rows[0] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create(activityId, { name, responsibleUserId, dueDate }) {
|
export async function create(activityId, { name, responsibleUserId, dueDate }) {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
'INSERT INTO subtasks (activity_id, name, responsible_user_id, due_date) VALUES (?, ?, ?, ?)',
|
'INSERT INTO tasks (activity_id, name, responsible_user_id, due_date) VALUES (?, ?, ?, ?)',
|
||||||
[activityId, name, responsibleUserId || null, dueDate || null]
|
[activityId, name, responsibleUserId || null, dueDate || null]
|
||||||
);
|
);
|
||||||
return findById(result.insertId);
|
return findById(result.insertId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update(id, { name, responsibleUserId, dueDate }) {
|
export async function update(id, { name, responsibleUserId, dueDate }) {
|
||||||
await pool.query('UPDATE subtasks SET name = ?, responsible_user_id = ?, due_date = ? WHERE id = ?', [
|
await pool.query('UPDATE tasks SET name = ?, responsible_user_id = ?, due_date = ? WHERE id = ?', [
|
||||||
name, responsibleUserId || null, dueDate || null, id,
|
name, responsibleUserId || null, dueDate || null, id,
|
||||||
]);
|
]);
|
||||||
return findById(id);
|
return findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setDone(id, isDone) {
|
export async function setDone(id, isDone) {
|
||||||
await pool.query('UPDATE subtasks SET is_done = ? WHERE id = ?', [isDone ? 1 : 0, id]);
|
await pool.query('UPDATE tasks SET is_done = ? WHERE id = ?', [isDone ? 1 : 0, id]);
|
||||||
return findById(id);
|
return findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remove(id) {
|
export async function remove(id) {
|
||||||
await pool.query('DELETE FROM subtasks WHERE id = ?', [id]);
|
await pool.query('DELETE FROM tasks WHERE id = ?', [id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countsByActivity(activityId) {
|
export async function countsByActivity(activityId) {
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
'SELECT COUNT(*) AS total, SUM(is_done) AS done FROM subtasks WHERE activity_id = ?',
|
'SELECT COUNT(*) AS total, SUM(is_done) AS done FROM tasks WHERE activity_id = ?',
|
||||||
[activityId]
|
[activityId]
|
||||||
);
|
);
|
||||||
return { total: rows[0].total, done: Number(rows[0].done) || 0 };
|
return { total: rows[0].total, done: Number(rows[0].done) || 0 };
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { requireAuth } from '../../middleware/auth.js';
|
||||||
|
import { getTasks, postTask, putTask, patchTaskDone, deleteTask } from './controller.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.use(requireAuth);
|
||||||
|
router.get('/activities/:activityId/tasks', getTasks);
|
||||||
|
router.post('/activities/:activityId/tasks', postTask);
|
||||||
|
router.put('/tasks/:id', putTask);
|
||||||
|
router.patch('/tasks/:id/done', patchTaskDone);
|
||||||
|
router.delete('/tasks/:id', deleteTask);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import * as repo from './repository.js';
|
||||||
|
import * as activitiesRepo from '../activities/repository.js';
|
||||||
|
import * as projectsRepo from '../projects/repository.js';
|
||||||
|
import { canWriteActivity, recalcFromTasks } from '../activities/service.js';
|
||||||
|
import { HttpError } from '../../utils/http.js';
|
||||||
|
import { logAudit } from '../../utils/audit.js';
|
||||||
|
|
||||||
|
async function requireActivityAccess(activityId, currentUser) {
|
||||||
|
const activity = await activitiesRepo.findById(activityId);
|
||||||
|
if (!activity) throw new HttpError(404, 'Actividad no encontrada');
|
||||||
|
const project = await projectsRepo.findById(activity.project_id);
|
||||||
|
if (!(await canWriteActivity(currentUser, project, activity))) {
|
||||||
|
throw new HttpError(403, 'No tienes permisos sobre esta actividad');
|
||||||
|
}
|
||||||
|
return activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listByActivity = (activityId) => repo.findByActivity(activityId);
|
||||||
|
|
||||||
|
export async function createTask(activityId, payload, currentUser) {
|
||||||
|
await requireActivityAccess(activityId, currentUser);
|
||||||
|
if (!payload.name) throw new HttpError(400, 'El nombre es obligatorio');
|
||||||
|
const task = await repo.create(activityId, payload);
|
||||||
|
await logAudit({ userId: currentUser.id, action: 'create', entityType: 'task', entityId: task.id });
|
||||||
|
const counts = await repo.countsByActivity(activityId);
|
||||||
|
await recalcFromTasks(activityId, counts.done, counts.total);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateTask(id, payload, currentUser) {
|
||||||
|
const task = await repo.findById(id);
|
||||||
|
if (!task) throw new HttpError(404, 'Tarea no encontrada');
|
||||||
|
await requireActivityAccess(task.activity_id, currentUser);
|
||||||
|
if (!payload.name) throw new HttpError(400, 'El nombre es obligatorio');
|
||||||
|
const updated = await repo.update(id, payload);
|
||||||
|
await logAudit({ userId: currentUser.id, action: 'update', entityType: 'task', entityId: id });
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setTaskDone(id, isDone, currentUser) {
|
||||||
|
const task = await repo.findById(id);
|
||||||
|
if (!task) throw new HttpError(404, 'Tarea no encontrada');
|
||||||
|
await requireActivityAccess(task.activity_id, currentUser);
|
||||||
|
const updated = await repo.setDone(id, isDone);
|
||||||
|
await logAudit({ userId: currentUser.id, action: isDone ? 'complete' : 'reopen', entityType: 'task', entityId: id });
|
||||||
|
const counts = await repo.countsByActivity(task.activity_id);
|
||||||
|
await recalcFromTasks(task.activity_id, counts.done, counts.total);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeTask(id, currentUser) {
|
||||||
|
const task = await repo.findById(id);
|
||||||
|
if (!task) throw new HttpError(404, 'Tarea no encontrada');
|
||||||
|
await requireActivityAccess(task.activity_id, currentUser);
|
||||||
|
await repo.remove(id);
|
||||||
|
await logAudit({ userId: currentUser.id, action: 'delete', entityType: 'task', entityId: id });
|
||||||
|
const counts = await repo.countsByActivity(task.activity_id);
|
||||||
|
await recalcFromTasks(task.activity_id, counts.done, counts.total);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-frontend",
|
"name": "project-linkedcorp-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -168,9 +168,9 @@ export const messages = {
|
|||||||
activity_dependencies: 'Dependencias',
|
activity_dependencies: 'Dependencias',
|
||||||
activity_add_dependency: 'Agregar dependencia',
|
activity_add_dependency: 'Agregar dependencia',
|
||||||
activity_depends_on: 'Depende de',
|
activity_depends_on: 'Depende de',
|
||||||
subtasks_title: 'Subtareas',
|
tasks_title: 'Tareas',
|
||||||
subtask_new: 'Nueva subtarea',
|
task_new: 'Nueva tarea',
|
||||||
subtask_done: 'Hecha',
|
task_done: 'Hecha',
|
||||||
|
|
||||||
due_on_time: 'En tiempo',
|
due_on_time: 'En tiempo',
|
||||||
due_warning: 'Proximo a vencer',
|
due_warning: 'Proximo a vencer',
|
||||||
@@ -376,9 +376,9 @@ export const messages = {
|
|||||||
activity_dependencies: 'Dependencies',
|
activity_dependencies: 'Dependencies',
|
||||||
activity_add_dependency: 'Add dependency',
|
activity_add_dependency: 'Add dependency',
|
||||||
activity_depends_on: 'Depends on',
|
activity_depends_on: 'Depends on',
|
||||||
subtasks_title: 'Subtasks',
|
tasks_title: 'Tasks',
|
||||||
subtask_new: 'New subtask',
|
task_new: 'New task',
|
||||||
subtask_done: 'Done',
|
task_done: 'Done',
|
||||||
|
|
||||||
due_on_time: 'On time',
|
due_on_time: 'On time',
|
||||||
due_warning: 'Due soon',
|
due_warning: 'Due soon',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Pencil, Save, X, Plus, Trash2, Link2 } from 'lucide-react';
|
|||||||
import { useUi } from '../../context/ui-context.jsx';
|
import { useUi } from '../../context/ui-context.jsx';
|
||||||
import { useAsync, useAction } from '../../hooks/useAsync.js';
|
import { useAsync, useAction } from '../../hooks/useAsync.js';
|
||||||
import { updateActivity, addDependency, removeDependency } from '../../services/activities.js';
|
import { updateActivity, addDependency, removeDependency } from '../../services/activities.js';
|
||||||
import { listSubtasks, createSubtask, setSubtaskDone, deleteSubtask } from '../../services/subtasks.js';
|
import { listTasks, createTask, setTaskDone, deleteTask } from '../../services/tasks.js';
|
||||||
import { listCatalog } from '../../services/catalogs.js';
|
import { listCatalog } from '../../services/catalogs.js';
|
||||||
import { listUsers } from '../../services/users.js';
|
import { listUsers } from '../../services/users.js';
|
||||||
import { Button } from '../../components/Button.jsx';
|
import { Button } from '../../components/Button.jsx';
|
||||||
@@ -29,17 +29,17 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
|||||||
const { data: priorities } = useAsync(() => listCatalog('priorities'), []);
|
const { data: priorities } = useAsync(() => listCatalog('priorities'), []);
|
||||||
const { data: statuses } = useAsync(() => listCatalog('activity-statuses'), []);
|
const { data: statuses } = useAsync(() => listCatalog('activity-statuses'), []);
|
||||||
const { data: users } = useAsync(listUsers, []);
|
const { data: users } = useAsync(listUsers, []);
|
||||||
const { data: subtasks, reload: reloadSubtasks } = useAsync(() => listSubtasks(activity.id), [activity.id]);
|
const { data: tasks, reload: reloadTasks } = useAsync(() => listTasks(activity.id), [activity.id]);
|
||||||
|
|
||||||
const [tab, setTab] = useState('detail');
|
const [tab, setTab] = useState('detail');
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [form, setForm] = useState(() => toForm(activity));
|
const [form, setForm] = useState(() => toForm(activity));
|
||||||
const [newSubtask, setNewSubtask] = useState('');
|
const [newTask, setNewTask] = useState('');
|
||||||
const [addingDep, setAddingDep] = useState('');
|
const [addingDep, setAddingDep] = useState('');
|
||||||
|
|
||||||
useEffect(() => { setForm(toForm(activity)); setEditing(false); }, [activity]);
|
useEffect(() => { setForm(toForm(activity)); setEditing(false); }, [activity]);
|
||||||
|
|
||||||
const hasSubtasks = subtasks && subtasks.length > 0;
|
const hasTasks = tasks && tasks.length > 0;
|
||||||
const priorityOptions = (priorities || []).map((p) => ({ value: p.id, label: p.name }));
|
const priorityOptions = (priorities || []).map((p) => ({ value: p.id, label: p.name }));
|
||||||
const statusOptions = (statuses || []).map((s) => ({ value: s.id, label: s.name }));
|
const statusOptions = (statuses || []).map((s) => ({ value: s.id, label: s.name }));
|
||||||
const userOptions = (users || []).map((u) => ({ value: u.id, label: u.name }));
|
const userOptions = (users || []).map((u) => ({ value: u.id, label: u.name }));
|
||||||
@@ -56,22 +56,22 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addSubtaskItem() {
|
async function addTaskItem() {
|
||||||
if (!newSubtask.trim()) return;
|
if (!newTask.trim()) return;
|
||||||
await runAction(async () => {
|
await runAction(async () => {
|
||||||
await createSubtask(activity.id, { name: newSubtask.trim() });
|
await createTask(activity.id, { name: newTask.trim() });
|
||||||
setNewSubtask('');
|
setNewTask('');
|
||||||
await reloadSubtasks();
|
await reloadTasks();
|
||||||
onChanged();
|
onChanged();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleSubtask(s) {
|
async function toggleTask(task) {
|
||||||
await runAction(async () => { await setSubtaskDone(s.id, !s.is_done); await reloadSubtasks(); onChanged(); });
|
await runAction(async () => { await setTaskDone(task.id, !task.is_done); await reloadTasks(); onChanged(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeSubtaskItem(s) {
|
async function removeTaskItem(task) {
|
||||||
await runAction(async () => { await deleteSubtask(s.id); await reloadSubtasks(); onChanged(); });
|
await runAction(async () => { await deleteTask(task.id); await reloadTasks(); onChanged(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addDep() {
|
async function addDep() {
|
||||||
@@ -88,7 +88,7 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
|||||||
<Tabs
|
<Tabs
|
||||||
tabs={[
|
tabs={[
|
||||||
{ key: 'detail', label: t('tab_info') },
|
{ key: 'detail', label: t('tab_info') },
|
||||||
{ key: 'subtasks', label: t('subtasks_title') },
|
{ key: 'tasks', label: t('tasks_title') },
|
||||||
{ key: 'dependencies', label: t('activity_dependencies') },
|
{ key: 'dependencies', label: t('activity_dependencies') },
|
||||||
{ key: 'comments', label: t('tab_comments') },
|
{ key: 'comments', label: t('tab_comments') },
|
||||||
{ key: 'documents', label: t('tab_documents') },
|
{ key: 'documents', label: t('tab_documents') },
|
||||||
@@ -167,7 +167,7 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
|||||||
{t('activity_milestone')}
|
{t('activity_milestone')}
|
||||||
</label>
|
</label>
|
||||||
</Field>
|
</Field>
|
||||||
{!hasSubtasks && (
|
{!hasTasks && (
|
||||||
<Field label={t('activity_progress')}>
|
<Field label={t('activity_progress')}>
|
||||||
<input className="input" type="number" min={0} max={100} value={form.progressPct} onChange={(e) => setForm((f) => ({ ...f, progressPct: e.target.value }))} />
|
<input className="input" type="number" min={0} max={100} value={form.progressPct} onChange={(e) => setForm((f) => ({ ...f, progressPct: e.target.value }))} />
|
||||||
</Field>
|
</Field>
|
||||||
@@ -178,19 +178,19 @@ export default function ActivityDetailPanel({ activity, projectId, siblingActivi
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === 'subtasks' && (
|
{tab === 'tasks' && (
|
||||||
<div>
|
<div>
|
||||||
{(subtasks || []).map((s) => (
|
{(tasks || []).map((task) => (
|
||||||
<div key={s.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 0' }}>
|
<div key={task.id} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 0' }}>
|
||||||
<input type="checkbox" checked={!!s.is_done} onChange={() => toggleSubtask(s)} />
|
<input type="checkbox" checked={!!task.is_done} onChange={() => toggleTask(task)} />
|
||||||
<span style={{ flex: 1, fontSize: '.8rem', textDecoration: s.is_done ? 'line-through' : 'none', opacity: s.is_done ? .6 : 1 }}>{s.name}</span>
|
<span style={{ flex: 1, fontSize: '.8rem', textDecoration: task.is_done ? 'line-through' : 'none', opacity: task.is_done ? .6 : 1 }}>{task.name}</span>
|
||||||
<button className="icon-btn" onClick={() => removeSubtaskItem(s)}><Trash2 size={15} /></button>
|
<button className="icon-btn" onClick={() => removeTaskItem(task)}><Trash2 size={15} /></button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{(!subtasks || subtasks.length === 0) && <div className="text-muted" style={{ fontSize: '.78rem' }}>{t('common_empty')}</div>}
|
{(!tasks || tasks.length === 0) && <div className="text-muted" style={{ fontSize: '.78rem' }}>{t('common_empty')}</div>}
|
||||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||||
<input className="input" value={newSubtask} placeholder={t('subtask_new')} onChange={(e) => setNewSubtask(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addSubtaskItem()} />
|
<input className="input" value={newTask} placeholder={t('task_new')} onChange={(e) => setNewTask(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addTaskItem()} />
|
||||||
<Button variant="standard" icon={Plus} onClick={addSubtaskItem}>{t('common_new')}</Button>
|
<Button variant="standard" icon={Plus} onClick={addTaskItem}>{t('common_new')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import { api } from './api.js';
|
|
||||||
|
|
||||||
export const listSubtasks = (activityId) => api.get(`/activities/${activityId}/subtasks`);
|
|
||||||
export const createSubtask = (activityId, data) => api.post(`/activities/${activityId}/subtasks`, data);
|
|
||||||
export const updateSubtask = (id, data) => api.put(`/subtasks/${id}`, data);
|
|
||||||
export const setSubtaskDone = (id, isDone) => api.patch(`/subtasks/${id}/done`, { isDone });
|
|
||||||
export const deleteSubtask = (id) => api.delete(`/subtasks/${id}`);
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { api } from './api.js';
|
||||||
|
|
||||||
|
export const listTasks = (activityId) => api.get(`/activities/${activityId}/tasks`);
|
||||||
|
export const createTask = (activityId, data) => api.post(`/activities/${activityId}/tasks`, data);
|
||||||
|
export const updateTask = (id, data) => api.put(`/tasks/${id}`, data);
|
||||||
|
export const setTaskDone = (id, isDone) => api.patch(`/tasks/${id}/done`, { isDone });
|
||||||
|
export const deleteTask = (id) => api.delete(`/tasks/${id}`);
|
||||||
Reference in New Issue
Block a user