feat: version 4.2.0 -- Informe de estado del proyecto con IA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,3 +18,7 @@ export const postProposeProject = asyncHandler(async (req, res) => {
|
||||
export const postCreateProject = asyncHandler(async (req, res) => {
|
||||
ok(res, await service.createNewProject(req.body || {}, req.currentUser), 201);
|
||||
});
|
||||
|
||||
export const postProjectReport = asyncHandler(async (req, res) => {
|
||||
ok(res, await service.generateProjectReport(Number(req.params.projectId), req.body?.lang, req.currentUser));
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Router } from 'express';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { requireAuth } from '../../middleware/auth.js';
|
||||
import { requireModule } from '../../middleware/modules.js';
|
||||
import { getStatus, postProjectPlan, postApplyProjectPlan, postProposeProject, postCreateProject } from './controller.js';
|
||||
import { getStatus, postProjectPlan, postApplyProjectPlan, postProposeProject, postCreateProject, postProjectReport } from './controller.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -14,6 +14,7 @@ router.get('/status', getStatus);
|
||||
router.post('/projects/:projectId/plan', requireModule('projects'), aiLimiter, postProjectPlan);
|
||||
router.post('/new-project/propose', requireModule('projects'), aiLimiter, postProposeProject);
|
||||
router.post('/new-project', requireModule('projects'), postCreateProject);
|
||||
router.post('/projects/:projectId/report', requireModule('projects'), aiLimiter, postProjectReport);
|
||||
router.post('/projects/:projectId/plan/apply', requireModule('projects'), postApplyProjectPlan);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -285,3 +285,78 @@ async function findCandidates(text, user) {
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, MAX_CANDIDATES);
|
||||
}
|
||||
|
||||
// --- Informe de estado del proyecto ---
|
||||
// Lee el estado real (actividades, tareas, fechas, avance, comentarios recientes) y la IA redacta
|
||||
// un informe para jefatura. Solo lectura: no modifica nada. Basta permiso de lectura del proyecto.
|
||||
|
||||
const REPORT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
summary: { type: 'string' },
|
||||
health: { type: 'string', enum: ['en_curso', 'en_riesgo', 'atrasado', 'completado', 'sin_iniciar'] },
|
||||
achievements: { type: 'array', items: { type: 'string' } },
|
||||
delays: { type: 'array', items: { type: 'object', properties: { item: { type: 'string' }, reason: { type: 'string' } }, required: ['item', 'reason'] } },
|
||||
risks: { type: 'array', items: { type: 'string' } },
|
||||
nextSteps: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: ['summary', 'health', 'achievements', 'delays', 'risks', 'nextSteps'],
|
||||
};
|
||||
|
||||
export async function generateProjectReport(projectId, lang, user) {
|
||||
const project = await getProject(projectId, user);
|
||||
const today = isoDate(new Date());
|
||||
const [activities] = await pool.query(
|
||||
`SELECT a.id, a.name, a.start_date, a.due_date, a.actual_end_date, a.progress_pct, s.name AS status_name, s.is_closed,
|
||||
u.name AS responsible,
|
||||
(SELECT COUNT(*) FROM tasks t WHERE t.activity_id = a.id AND t.deleted_at IS NULL) AS tasks_total,
|
||||
(SELECT COUNT(*) FROM tasks t WHERE t.activity_id = a.id AND t.deleted_at IS NULL AND t.is_done = 1) AS tasks_done,
|
||||
(SELECT GROUP_CONCAT(t.name SEPARATOR '; ') FROM tasks t WHERE t.activity_id = a.id AND t.deleted_at IS NULL
|
||||
AND t.is_done = 0 AND t.due_date < CURDATE()) AS overdue_tasks
|
||||
FROM activities a JOIN activity_statuses s ON s.id = a.status_id LEFT JOIN users u ON u.id = a.responsible_user_id
|
||||
WHERE a.project_id = ? AND a.deleted_at IS NULL ORDER BY a.sort_order, a.id`,
|
||||
[projectId]
|
||||
);
|
||||
const [comments] = await pool.query(
|
||||
`SELECT c.message, c.created_at, u.name AS author FROM comments c LEFT JOIN users u ON u.id = c.user_id
|
||||
WHERE (c.entity_type = 'project' AND c.entity_id = ?)
|
||||
OR (c.entity_type = 'activity' AND c.entity_id IN (SELECT id FROM activities WHERE project_id = ? AND deleted_at IS NULL))
|
||||
ORDER BY c.created_at DESC LIMIT 15`,
|
||||
[projectId, projectId]
|
||||
);
|
||||
const fmt = (d) => (d ? String(d).slice(0, 10) : '-');
|
||||
const lines = activities.map((a) => [
|
||||
`- ${clip(a.name, 150)} | estado: ${a.status_name} | avance: ${a.progress_pct || 0}% | ${fmt(a.start_date)} -> ${fmt(a.due_date)}`,
|
||||
a.actual_end_date ? ` | terminada: ${fmt(a.actual_end_date)}` : '',
|
||||
` | responsable: ${a.responsible || 'sin asignar'} | tareas: ${a.tasks_done}/${a.tasks_total}`,
|
||||
a.overdue_tasks ? ` | tareas vencidas: ${clip(a.overdue_tasks, 300)}` : '',
|
||||
].join(''));
|
||||
const result = await askStructured({
|
||||
system: `Eres un jefe de proyectos de TI que redacta informes de estado claros y honestos para la jefatura. Usa solo los datos dados, no inventes hechos ni cifras. Se concreto: menciona actividades y fechas. Responde en ${languageFor(lang)}.`,
|
||||
prompt: [
|
||||
`Fecha de hoy: ${today}`,
|
||||
`Proyecto: ${project.code} ${project.name} | estado: ${project.status_name || '-'} | avance: ${project.progress_pct ?? 0}%`,
|
||||
`Fechas: inicio ${fmt(project.start_date)}, fin estimado ${fmt(project.estimated_end_date)}`,
|
||||
project.objective ? `Objetivo: ${clip(project.objective, 500)}` : '',
|
||||
activities.length ? `Actividades:\n${lines.join('\n')}` : 'El proyecto aun no tiene actividades.',
|
||||
comments.length ? `Comentarios recientes:\n${comments.map((c) => `- ${fmt(c.created_at)} ${c.author || ''}: ${clip(c.message, 300)}`).join('\n')}` : '',
|
||||
'Redacta: summary (3-5 oraciones con la situacion general), health, achievements (lo completado o avanzado), delays (lo atrasado o vencido con su motivo probable segun los datos), risks y nextSteps (acciones concretas para la proxima semana). Listas vacias si no aplica.',
|
||||
].filter(Boolean).join('\n'),
|
||||
toolName: 'informe_de_estado',
|
||||
schema: REPORT_SCHEMA,
|
||||
maxTokens: 8000,
|
||||
usage: { userId: user.id, action: 'project_report' },
|
||||
});
|
||||
await logAudit({ userId: user.id, action: 'ai_report', entityType: 'project', entityId: projectId });
|
||||
const list = (v) => (Array.isArray(v) ? v.map((x) => clip(x, 500)).filter(Boolean).slice(0, 15) : []);
|
||||
return {
|
||||
project: { code: project.code, name: project.name },
|
||||
generatedAt: new Date().toISOString(),
|
||||
summary: clip(result.summary, 2000),
|
||||
health: result.health,
|
||||
achievements: list(result.achievements),
|
||||
delays: (Array.isArray(result.delays) ? result.delays : []).slice(0, 15).map((d) => ({ item: clip(d?.item, 300), reason: clip(d?.reason, 500) })).filter((d) => d.item),
|
||||
risks: list(result.risks),
|
||||
nextSteps: list(result.nextSteps),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user