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:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "project-linkedcorp-frontend",
|
||||
"private": true,
|
||||
"version": "4.1.0",
|
||||
"version": "4.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -63,6 +63,27 @@ export const messages = {
|
||||
ai_usage_total: 'Total',
|
||||
ai_usage_hint: 'costo estimado; el monto exacto está en la consola de Anthropic.',
|
||||
ai_usage_empty: 'Todavía no hay consumo registrado.',
|
||||
ai_report_button: 'Informe con IA',
|
||||
ai_report_title: 'Informe de estado',
|
||||
ai_report_busy: 'La IA está redactando el informe...',
|
||||
ai_report_date: 'Generado',
|
||||
ai_report_health: 'Estado general',
|
||||
ai_report_summary: 'Resumen',
|
||||
ai_report_achievements: 'Avances',
|
||||
ai_report_delays: 'Atrasos',
|
||||
ai_report_risks: 'Riesgos',
|
||||
ai_report_next: 'Próximos pasos',
|
||||
ai_report_disclaimer: 'Redactado por IA a partir de los datos del sistema; revísalo antes de enviarlo.',
|
||||
ai_report_copy: 'Copiar',
|
||||
ai_report_download: 'Descargar .txt',
|
||||
ai_report_regenerate: 'Volver a generar',
|
||||
ai_report_copied: 'Informe copiado al portapapeles',
|
||||
ai_report_copy_failed: 'No se pudo copiar; usa Descargar.',
|
||||
ai_health_en_curso: 'En curso',
|
||||
ai_health_en_riesgo: 'En riesgo',
|
||||
ai_health_atrasado: 'Atrasado',
|
||||
ai_health_completado: 'Completado',
|
||||
ai_health_sin_iniciar: 'Sin iniciar',
|
||||
nav_projects: 'Proyectos',
|
||||
nav_organization: 'Organizacion',
|
||||
nav_config: 'Configuracion',
|
||||
@@ -643,6 +664,27 @@ export const messages = {
|
||||
ai_usage_total: 'Total',
|
||||
ai_usage_hint: 'estimated cost; the exact amount is in the Anthropic console.',
|
||||
ai_usage_empty: 'No usage recorded yet.',
|
||||
ai_report_button: 'AI report',
|
||||
ai_report_title: 'Status report',
|
||||
ai_report_busy: 'AI is writing the report...',
|
||||
ai_report_date: 'Generated',
|
||||
ai_report_health: 'Overall status',
|
||||
ai_report_summary: 'Summary',
|
||||
ai_report_achievements: 'Progress',
|
||||
ai_report_delays: 'Delays',
|
||||
ai_report_risks: 'Risks',
|
||||
ai_report_next: 'Next steps',
|
||||
ai_report_disclaimer: 'Written by AI from system data; review it before sending.',
|
||||
ai_report_copy: 'Copy',
|
||||
ai_report_download: 'Download .txt',
|
||||
ai_report_regenerate: 'Regenerate',
|
||||
ai_report_copied: 'Report copied to clipboard',
|
||||
ai_report_copy_failed: 'Could not copy; use Download.',
|
||||
ai_health_en_curso: 'On track',
|
||||
ai_health_en_riesgo: 'At risk',
|
||||
ai_health_atrasado: 'Delayed',
|
||||
ai_health_completado: 'Completed',
|
||||
ai_health_sin_iniciar: 'Not started',
|
||||
nav_projects: 'Projects',
|
||||
nav_organization: 'Organization',
|
||||
nav_config: 'Settings',
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Copy, Download, RefreshCw } from 'lucide-react';
|
||||
import { DetailPopup } from '../../components/DetailPopup.jsx';
|
||||
import { Button } from '../../components/Button.jsx';
|
||||
import { Badge } from '../../components/Badge.jsx';
|
||||
import { InfoBar } from '../../components/InfoBar.jsx';
|
||||
import { BusyPanel } from '../../components/BusyPanel.jsx';
|
||||
import { useUi } from '../../context/ui-context.jsx';
|
||||
import { generateProjectReport } from '../../services/ai.js';
|
||||
|
||||
const HEALTH_COLOR = { en_curso: 'success', en_riesgo: 'warning', atrasado: 'danger', completado: 'success', sin_iniciar: 'muted' };
|
||||
|
||||
// Texto plano del informe, para copiar a un correo o descargar.
|
||||
function reportToText(r, t) {
|
||||
const section = (title, items) => (items.length ? `\n${title}\n${items.map((x) => `- ${x}`).join('\n')}\n` : '');
|
||||
return [
|
||||
`${t('ai_report_title')}: ${r.project.code} — ${r.project.name}`,
|
||||
`${t('ai_report_date')}: ${new Date(r.generatedAt).toLocaleString()}`,
|
||||
`${t('ai_report_health')}: ${t(`ai_health_${r.health}`)}`,
|
||||
`\n${t('ai_report_summary')}\n${r.summary}\n`,
|
||||
section(t('ai_report_achievements'), r.achievements),
|
||||
section(t('ai_report_delays'), r.delays.map((d) => `${d.item}: ${d.reason}`)),
|
||||
section(t('ai_report_risks'), r.risks),
|
||||
section(t('ai_report_next'), r.nextSteps),
|
||||
].join('\n').replace(/\n{3,}/g, '\n\n');
|
||||
}
|
||||
|
||||
// Informe de estado redactado por la IA a partir de los datos reales del proyecto (solo lectura).
|
||||
export function ProjectReportDialog({ open, projectId, onClose }) {
|
||||
const { t, lang, pushToast } = useUi();
|
||||
const [report, setReport] = useState(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
async function generate() {
|
||||
setBusy(true); setError('');
|
||||
try {
|
||||
setReport(await generateProjectReport(projectId, { lang }));
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Se genera al abrir (no al montar la pagina): cada informe es una consulta a la IA.
|
||||
useEffect(() => {
|
||||
if (open && !report && !busy) generate();
|
||||
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(reportToText(report, t));
|
||||
pushToast({ tone: 'success', message: t('ai_report_copied') });
|
||||
} catch {
|
||||
pushToast({ tone: 'error', message: t('ai_report_copy_failed') });
|
||||
}
|
||||
}
|
||||
|
||||
function download() {
|
||||
const blob = new Blob([reportToText(report, t)], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `informe-${report.project.code}-${report.generatedAt.slice(0, 10)}.txt`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
const List = ({ title, items }) => (items.length ? (
|
||||
<>
|
||||
<h4 style={{ margin: '14px 0 4px' }}>{title}</h4>
|
||||
<ul style={{ margin: 0, paddingLeft: 18, fontSize: '.88rem' }}>{items.map((x, i) => <li key={i}>{x}</li>)}</ul>
|
||||
</>
|
||||
) : null);
|
||||
|
||||
return (
|
||||
<DetailPopup open={open} title={t('ai_report_title')} onClose={busy ? () => {} : onClose} width={760} height={680}>
|
||||
{error && <InfoBar tone="error">{error}</InfoBar>}
|
||||
{busy && <BusyPanel message={t('ai_report_busy')} hint={t('ai_busy_hint')} />}
|
||||
{report && !busy && (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 8 }}>
|
||||
<Badge color={HEALTH_COLOR[report.health] || 'muted'}>{t(`ai_health_${report.health}`)}</Badge>
|
||||
<span className="text-muted" style={{ fontSize: '.8rem' }}>{t('ai_report_date')}: {new Date(report.generatedAt).toLocaleString()}</span>
|
||||
</div>
|
||||
<h4 style={{ margin: '8px 0 4px' }}>{t('ai_report_summary')}</h4>
|
||||
<p style={{ margin: 0, fontSize: '.9rem' }}>{report.summary}</p>
|
||||
<List title={t('ai_report_achievements')} items={report.achievements} />
|
||||
<List title={t('ai_report_delays')} items={report.delays.map((d) => `${d.item}: ${d.reason}`)} />
|
||||
<List title={t('ai_report_risks')} items={report.risks} />
|
||||
<List title={t('ai_report_next')} items={report.nextSteps} />
|
||||
<p className="text-muted" style={{ fontSize: '.75rem', marginTop: 14 }}>{t('ai_report_disclaimer')}</p>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<Button variant="accent" icon={Copy} onClick={copy}>{t('ai_report_copy')}</Button>
|
||||
<Button variant="standard" icon={Download} onClick={download}>{t('ai_report_download')}</Button>
|
||||
<Button variant="standard" icon={RefreshCw} onClick={generate}>{t('ai_report_regenerate')}</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DetailPopup>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { ArrowLeft, FileText } from 'lucide-react';
|
||||
import { ProjectReportDialog } from './ProjectReportDialog.jsx';
|
||||
import { getAiStatus } from '../../services/ai.js';
|
||||
import { useUi } from '../../context/ui-context.jsx';
|
||||
import { useAsync } from '../../hooks/useAsync.js';
|
||||
import { useUserDrafts } from '../../hooks/useDraftForm.js';
|
||||
@@ -44,6 +47,9 @@ export default function ProyectoDetallePage() {
|
||||
const deepLinkActivityTab = searchParams.get('activityTab') || undefined;
|
||||
|
||||
const { data: project, reload } = useAsync(() => getProject(projectId), [projectId]);
|
||||
// Informe con IA: solo si el superadmin configuro la clave (el backend valida lectura del proyecto).
|
||||
const { data: aiStatus } = useAsync(() => getAiStatus().catch(() => ({ enabled: false })), []);
|
||||
const [reportOpen, setReportOpen] = useState(false);
|
||||
|
||||
if (!project) return <div className="text-muted">{t('common_loading')}</div>;
|
||||
|
||||
@@ -55,7 +61,13 @@ export default function ProyectoDetallePage() {
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
|
||||
<h2 className="page-title mt-0">{project.code} — {project.name}</h2>
|
||||
<StatusBadge name={project.status_name} color={project.status_color} />
|
||||
{aiStatus?.enabled && (
|
||||
<div style={{ marginLeft: 'auto' }}>
|
||||
<Button variant="standard" icon={FileText} onClick={() => setReportOpen(true)}>{t('ai_report_button')}</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ProjectReportDialog open={reportOpen} projectId={projectId} onClose={() => setReportOpen(false)} />
|
||||
|
||||
<Tabs
|
||||
tabs={[
|
||||
|
||||
@@ -5,3 +5,4 @@ export const generateProjectPlan = (projectId, data) => api.post(`/ai/projects/$
|
||||
export const applyProjectPlan = (projectId, activities) => api.post(`/ai/projects/${projectId}/plan/apply`, { activities });
|
||||
export const proposeNewProject = (data) => api.post('/ai/new-project/propose', data);
|
||||
export const createNewProject = (data) => api.post('/ai/new-project', data);
|
||||
export const generateProjectReport = (projectId, data) => api.post(`/ai/projects/${projectId}/report`, data);
|
||||
|
||||
Reference in New Issue
Block a user