diff --git a/frontend/src/i18n/messages.js b/frontend/src/i18n/messages.js index 3785d48..b094e27 100644 --- a/frontend/src/i18n/messages.js +++ b/frontend/src/i18n/messages.js @@ -77,6 +77,7 @@ export const messages = { risk_high: 'Alto', tab_info: 'Info', + tab_members: 'Miembros', tab_activities: 'Actividades', tab_gantt: 'Gantt', tab_calendar: 'Calendario', @@ -211,6 +212,7 @@ export const messages = { risk_high: 'High', tab_info: 'Info', + tab_members: 'Members', tab_activities: 'Activities', tab_gantt: 'Gantt', tab_calendar: 'Calendar', diff --git a/frontend/src/pages/Proyectos/ProjectInfoTab.jsx b/frontend/src/pages/Proyectos/ProjectInfoTab.jsx index fce0c29..d16a01d 100644 --- a/frontend/src/pages/Proyectos/ProjectInfoTab.jsx +++ b/frontend/src/pages/Proyectos/ProjectInfoTab.jsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react'; -import { Pencil, Save, X, UserPlus, Trash2 } from 'lucide-react'; +import { Pencil, Save, X } from 'lucide-react'; import { useUi } from '../../context/ui-context.jsx'; import { useAsync, useAction } from '../../hooks/useAsync.js'; -import { updateProject, addProjectMember, removeProjectMember } from '../../services/projects.js'; +import { updateProject } from '../../services/projects.js'; import { listCatalog } from '../../services/catalogs.js'; import { listDepartments } from '../../services/departments.js'; import { listUsers } from '../../services/users.js'; @@ -11,7 +11,6 @@ import { Field, FieldRow } from '../../components/Field.jsx'; import { ComboBox } from '../../components/ComboBox.jsx'; import { StatusBadge, ProgressBar } from '../../components/StatusBadge.jsx'; import { Badge } from '../../components/Badge.jsx'; -import { DetailPopup } from '../../components/DetailPopup.jsx'; function flatten(nodes, acc = []) { for (const n of nodes) { acc.push(n); if (n.children?.length) flatten(n.children, acc); } @@ -32,7 +31,7 @@ function toForm(p) { } export default function ProjectInfoTab({ project, onChanged }) { - const { t, pushToast, openDialog } = useUi(); + const { t, pushToast } = useUi(); const runAction = useAction(); const { data: priorities } = useAsync(() => listCatalog('priorities'), []); const { data: statuses } = useAsync(() => listCatalog('project-statuses'), []); @@ -42,8 +41,6 @@ export default function ProjectInfoTab({ project, onChanged }) { const [editing, setEditing] = useState(false); const [form, setForm] = useState(() => toForm(project)); - const [addingMember, setAddingMember] = useState(''); - const [addMemberOpen, setAddMemberOpen] = useState(false); useEffect(() => { setForm(toForm(project)); }, [project]); @@ -51,7 +48,6 @@ export default function ProjectInfoTab({ project, onChanged }) { const priorityOptions = (priorities || []).map((p) => ({ value: p.id, label: p.name })); const statusOptions = (statuses || []).map((s) => ({ value: s.id, label: s.name })); const typeOptions = (types || []).map((tp) => ({ value: tp.id, label: tp.name })); - const userOptions = (users || []).filter((u) => !project.members.some((m) => m.user_id === u.id)).map((u) => ({ value: u.id, label: u.name })); async function save() { const changed = JSON.stringify(form) !== JSON.stringify(toForm(project)); @@ -72,28 +68,6 @@ export default function ProjectInfoTab({ project, onChanged }) { }); } - async function addMember() { - if (!addingMember) return; - await runAction(async () => { - await addProjectMember(project.id, Number(addingMember)); - setAddingMember(''); - setAddMemberOpen(false); - onChanged(); - }); - } - - function confirmRemoveMember(member) { - openDialog({ - tone: 'warning', - title: t('common_confirm_delete_title'), - message: t('common_confirm_delete_msg'), - actions: [ - { label: t('common_cancel'), variant: 'standard' }, - { label: t('common_delete'), variant: 'danger', onClick: () => runAction(async () => { await removeProjectMember(project.id, member.user_id); onChanged(); }) }, - ], - }); - } - return (
@@ -187,29 +161,6 @@ export default function ProjectInfoTab({ project, onChanged }) {
)} -
-
-

{t('project_members')}

- -
- {project.members.map((m) => ( -
- {m.name} · {m.email} - -
- ))} - {project.members.length === 0 &&
{t('common_empty')}
} -
- - setAddMemberOpen(false)} width={420} height={260}> - - - -
- - -
-
); } diff --git a/frontend/src/pages/Proyectos/ProjectMembersTab.jsx b/frontend/src/pages/Proyectos/ProjectMembersTab.jsx new file mode 100644 index 0000000..5681b09 --- /dev/null +++ b/frontend/src/pages/Proyectos/ProjectMembersTab.jsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; +import { UserPlus, Trash2 } from 'lucide-react'; +import { useUi } from '../../context/ui-context.jsx'; +import { useAsync, useAction } from '../../hooks/useAsync.js'; +import { addProjectMember, removeProjectMember } from '../../services/projects.js'; +import { listUsers } from '../../services/users.js'; +import { Button } from '../../components/Button.jsx'; +import { Field } from '../../components/Field.jsx'; +import { ComboBox } from '../../components/ComboBox.jsx'; +import { DetailPopup } from '../../components/DetailPopup.jsx'; + +export default function ProjectMembersTab({ project, onChanged }) { + const { t, openDialog } = useUi(); + const runAction = useAction(); + const { data: users } = useAsync(listUsers, []); + + const [addingMember, setAddingMember] = useState(''); + const [addMemberOpen, setAddMemberOpen] = useState(false); + + const userOptions = (users || []).filter((u) => !project.members.some((m) => m.user_id === u.id)).map((u) => ({ value: u.id, label: u.name })); + + async function addMember() { + if (!addingMember) return; + await runAction(async () => { + await addProjectMember(project.id, Number(addingMember)); + setAddingMember(''); + setAddMemberOpen(false); + onChanged(); + }); + } + + function confirmRemoveMember(member) { + openDialog({ + tone: 'warning', + title: t('common_confirm_delete_title'), + message: t('common_confirm_delete_msg'), + actions: [ + { label: t('common_cancel'), variant: 'standard' }, + { label: t('common_delete'), variant: 'danger', onClick: () => runAction(async () => { await removeProjectMember(project.id, member.user_id); onChanged(); }) }, + ], + }); + } + + return ( +
+
+
+

{t('project_members')}

+ +
+ {project.members.map((m) => ( +
+ {m.name} · {m.email} + +
+ ))} + {project.members.length === 0 &&
{t('common_empty')}
} +
+ + setAddMemberOpen(false)} width={420} height={260}> + + + +
+ + +
+
+
+ ); +} diff --git a/frontend/src/pages/Proyectos/ProyectoDetallePage.jsx b/frontend/src/pages/Proyectos/ProyectoDetallePage.jsx index 12c781f..8ef3260 100644 --- a/frontend/src/pages/Proyectos/ProyectoDetallePage.jsx +++ b/frontend/src/pages/Proyectos/ProyectoDetallePage.jsx @@ -10,6 +10,7 @@ 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'; @@ -38,6 +39,7 @@ export default function ProyectoDetallePage() { {tab === 'info' && } + {tab === 'members' && } {tab === 'activities' && } {tab === 'gantt' && } {tab === 'calendar' && }