9d50bc8564
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
5.2 KiB
React
98 lines
5.2 KiB
React
import { useState } from 'react';
|
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
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';
|
|
import { getProject } from '../../services/projects.js';
|
|
import { Button } from '../../components/Button.jsx';
|
|
import { Tabs } from '../../components/Tabs.jsx';
|
|
import { StatusBadge } from '../../components/StatusBadge.jsx';
|
|
import { CommentsPanel } from '../../components/CommentsPanel.jsx';
|
|
import { AttachmentsPanel } from '../../components/AttachmentsPanel.jsx';
|
|
import ProjectInfoTab from './ProjectInfoTab.jsx';
|
|
import ProjectMembersTab from './ProjectMembersTab.jsx';
|
|
import ProjectActivitiesTab from './ProjectActivitiesTab.jsx';
|
|
import ProjectGanttTab from './ProjectGanttTab.jsx';
|
|
import ProjectCalendarTab from './ProjectCalendarTab.jsx';
|
|
|
|
export default function ProyectoDetallePage() {
|
|
const { id } = useParams();
|
|
const projectId = Number(id);
|
|
const { t } = useUi();
|
|
const navigate = useNavigate();
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
// La pestaña vive en la URL (?tab=): asi el retorno tras el login (o un F5) vuelve a la misma
|
|
// pestaña, y el aviso de cambios sin guardar intercepta el cambio de pestaña.
|
|
// Llegada desde el buscador (Proyectos): ?tab=activities&activity=<id>&activityTab=tasks abre
|
|
// directo la actividad (y su pestaña de tareas) que coincidio con la busqueda.
|
|
const tab = searchParams.get('tab') || 'info';
|
|
const setTab = (key) => setSearchParams((prev) => {
|
|
const next = new URLSearchParams(prev);
|
|
next.set('tab', key);
|
|
next.delete('activity');
|
|
next.delete('activityTab');
|
|
return next;
|
|
}, { replace: true });
|
|
// Pestañas con borradores pendientes de este proyecto (el borrador guarda la URL con su ?tab=).
|
|
const drafts = useUserDrafts();
|
|
const dirtyTab = (key) => drafts.some((d) => {
|
|
if (typeof d.path !== 'string') return false;
|
|
const [path, query = ''] = d.path.split('?');
|
|
return path === `/proyectos/${projectId}` && (new URLSearchParams(query).get('tab') || 'info') === key;
|
|
});
|
|
const deepLinkActivityId = searchParams.get('activity') ? Number(searchParams.get('activity')) : null;
|
|
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>;
|
|
|
|
return (
|
|
<div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
|
<Button variant="subtle" icon={ArrowLeft} onClick={() => navigate('/proyectos')}>{t('projects_title')}</Button>
|
|
</div>
|
|
<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={[
|
|
{ key: 'info', label: t('tab_info'), dirty: dirtyTab('info') },
|
|
{ key: 'members', dirty: dirtyTab('members'), label: t('tab_members'), count: project.members_count },
|
|
{ key: 'activities', dirty: dirtyTab('activities'), label: t('tab_activities'), count: `${project.activities_complete}/${project.activities_total}` },
|
|
{ key: 'gantt', label: t('tab_gantt') },
|
|
{ key: 'calendar', label: t('tab_calendar') },
|
|
{ key: 'comments', dirty: dirtyTab('comments'), label: t('tab_comments'), count: project.comments_count },
|
|
{ key: 'documents', dirty: dirtyTab('documents'), label: t('tab_documents'), count: project.documents_count },
|
|
]}
|
|
active={tab}
|
|
onChange={setTab}
|
|
/>
|
|
|
|
{tab === 'info' && <ProjectInfoTab project={project} onChanged={reload} />}
|
|
{tab === 'members' && <ProjectMembersTab project={project} onChanged={reload} />}
|
|
{tab === 'activities' && (
|
|
<ProjectActivitiesTab project={project} projectId={projectId} openActivityId={deepLinkActivityId} openActivityTab={deepLinkActivityTab} />
|
|
)}
|
|
{tab === 'gantt' && <ProjectGanttTab projectId={projectId} />}
|
|
{tab === 'calendar' && <ProjectCalendarTab projectId={projectId} />}
|
|
{tab === 'comments' && <CommentsPanel entityType="project" entityId={projectId} onChanged={reload} />}
|
|
{tab === 'documents' && <AttachmentsPanel entityType="project" entityId={projectId} onChanged={reload} />}
|
|
</div>
|
|
);
|
|
}
|