feat(proyectos): Miembros pasa a ser su propia pestaña junto a Info
Antes la lista de miembros del proyecto vivia dentro de la pestaña Info, mezclada con los campos del proyecto. Se extrae a un componente propio (ProjectMembersTab) y se agrega como pestaña "Miembros" justo a la derecha de "Info" en el detalle del proyecto. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<div>
|
||||
<div className="panel-card">
|
||||
<div className="toolbar">
|
||||
<h3 style={{ margin: 0 }}>{t('project_members')}</h3>
|
||||
<Button variant="accent" icon={UserPlus} onClick={() => setAddMemberOpen(true)}>{t('project_add_member')}</Button>
|
||||
</div>
|
||||
{project.members.map((m) => (
|
||||
<div key={m.user_id} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 0' }}>
|
||||
<span style={{ flex: 1, fontSize: '.82rem' }}>{m.name} <span className="text-muted">· {m.email}</span></span>
|
||||
<button className="icon-btn" onClick={() => confirmRemoveMember(m)}><Trash2 size={16} /></button>
|
||||
</div>
|
||||
))}
|
||||
{project.members.length === 0 && <div className="text-muted" style={{ fontSize: '.78rem' }}>{t('common_empty')}</div>}
|
||||
</div>
|
||||
|
||||
<DetailPopup open={addMemberOpen} title={t('project_add_member')} onClose={() => setAddMemberOpen(false)} width={420} height={260}>
|
||||
<Field label={t('project_add_member')}>
|
||||
<ComboBox options={userOptions} value={addingMember} onChange={setAddingMember} placeholder={t('project_add_member')} />
|
||||
</Field>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
|
||||
<Button variant="accent" icon={UserPlus} onClick={addMember}>{t('common_save')}</Button>
|
||||
<Button variant="standard" onClick={() => setAddMemberOpen(false)}>{t('common_cancel')}</Button>
|
||||
</div>
|
||||
</DetailPopup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user