feat: version 3.0.0 -- Correcciones de auditoria de seguridad, accesibilidad y redundancia
Permisos (superadmin, viewer, busqueda, tareas recurrentes), bootstrap atomico, revocacion de JWT por version de sesion (028), control optimista de edicion (029), logo sin SVG + CSP, ingesta endurecida, correos escapados, borradores, paginacion de proyectos, accesibilidad de dialogos/controles e i18n. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
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';
|
||||
@@ -20,10 +20,26 @@ export default function ProyectoDetallePage() {
|
||||
const projectId = Number(id);
|
||||
const { t } = useUi();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
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, setTab] = useState(searchParams.get('tab') || 'info');
|
||||
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;
|
||||
|
||||
@@ -43,13 +59,13 @@ export default function ProyectoDetallePage() {
|
||||
|
||||
<Tabs
|
||||
tabs={[
|
||||
{ key: 'info', label: t('tab_info') },
|
||||
{ key: 'members', label: t('tab_members'), count: project.members_count },
|
||||
{ key: 'activities', label: t('tab_activities'), count: `${project.activities_complete}/${project.activities_total}` },
|
||||
{ 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', label: t('tab_comments'), count: project.comments_count },
|
||||
{ key: 'documents', label: t('tab_documents'), count: project.documents_count },
|
||||
{ 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}
|
||||
|
||||
Reference in New Issue
Block a user