fix(ui): elimina los 2 casos que quedaban de formulario debajo de un listado

Auditoria completa del frontend: quedaban 2 lugares con el mismo antipatron que
ya se habia corregido en Proyectos/Departamentos/Usuarios/Catalogos.

- ProjectActivitiesTab: "Nueva actividad" se apilaba debajo del listado de
  actividades. Ahora abre en su propio DetailPopup, igual que el resto.
- ProjectInfoTab (pestaña Info de un proyecto): el combo "agregar miembro" se
  apilaba debajo de la lista de miembros. Ahora hay un boton "Agregar miembro"
  en el encabezado de esa tarjeta que abre un DetailPopup chico.

Con esto no queda ningun listado del sistema con un formulario apilado debajo:
todos usan DetailPopup (ver regla en project-md-templates/UI-UX-WINDOWS.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-03 11:39:35 -05:00
parent b172a8f087
commit 2661d31977
2 changed files with 52 additions and 44 deletions
@@ -88,46 +88,43 @@ export default function ProjectActivitiesTab({ projectId }) {
onRowClick={openActivity}
/>
{creating && (
<div className="panel-card" style={{ marginTop: 16 }}>
<h3>{t('activity_new')}</h3>
<Field label={t('common_name')}>
<input className="input" value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} autoFocus />
<DetailPopup open={creating} title={t('activity_new')} onClose={() => setCreating(false)} width={640} height={560}>
<Field label={t('common_name')}>
<input className="input" value={form.name} onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))} autoFocus />
</Field>
<Field label={t('common_description')}>
<textarea className="textarea" value={form.description} onChange={(e) => setForm((f) => ({ ...f, description: e.target.value }))} />
</Field>
<FieldRow>
<Field label={t('common_responsible')}>
<ComboBox options={[{ value: '', label: '—' }, ...userOptions]} value={form.responsibleUserId} onChange={(v) => setForm((f) => ({ ...f, responsibleUserId: v }))} />
</Field>
<Field label={t('common_description')}>
<textarea className="textarea" value={form.description} onChange={(e) => setForm((f) => ({ ...f, description: e.target.value }))} />
<Field label={t('common_priority')}>
<ComboBox options={priorityOptions} value={form.priorityId} onChange={(v) => setForm((f) => ({ ...f, priorityId: v }))} />
</Field>
<FieldRow>
<Field label={t('common_responsible')}>
<ComboBox options={[{ value: '', label: '—' }, ...userOptions]} value={form.responsibleUserId} onChange={(v) => setForm((f) => ({ ...f, responsibleUserId: v }))} />
</Field>
<Field label={t('common_priority')}>
<ComboBox options={priorityOptions} value={form.priorityId} onChange={(v) => setForm((f) => ({ ...f, priorityId: v }))} />
</Field>
<Field label={t('common_status')}>
<ComboBox options={statusOptions} value={form.statusId} onChange={(v) => setForm((f) => ({ ...f, statusId: v }))} />
</Field>
</FieldRow>
<FieldRow>
<Field label={t('activity_start_date')}>
<input className="input" type="date" value={form.startDate} onChange={(e) => setForm((f) => ({ ...f, startDate: e.target.value }))} />
</Field>
<Field label={t('activity_due_date')}>
<input className="input" type="date" value={form.dueDate} onChange={(e) => setForm((f) => ({ ...f, dueDate: e.target.value }))} />
</Field>
<Field label={t('activity_milestone')}>
<label className="checkbox-row">
<input type="checkbox" checked={form.isMilestone} onChange={(e) => setForm((f) => ({ ...f, isMilestone: e.target.checked }))} />
{t('activity_milestone')}
</label>
</Field>
</FieldRow>
<div style={{ display: 'flex', gap: 8 }}>
<Button variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
<Button variant="standard" onClick={() => setCreating(false)}>{t('common_cancel')}</Button>
</div>
<Field label={t('common_status')}>
<ComboBox options={statusOptions} value={form.statusId} onChange={(v) => setForm((f) => ({ ...f, statusId: v }))} />
</Field>
</FieldRow>
<FieldRow>
<Field label={t('activity_start_date')}>
<input className="input" type="date" value={form.startDate} onChange={(e) => setForm((f) => ({ ...f, startDate: e.target.value }))} />
</Field>
<Field label={t('activity_due_date')}>
<input className="input" type="date" value={form.dueDate} onChange={(e) => setForm((f) => ({ ...f, dueDate: e.target.value }))} />
</Field>
<Field label={t('activity_milestone')}>
<label className="checkbox-row">
<input type="checkbox" checked={form.isMilestone} onChange={(e) => setForm((f) => ({ ...f, isMilestone: e.target.checked }))} />
{t('activity_milestone')}
</label>
</Field>
</FieldRow>
<div style={{ display: 'flex', gap: 8 }}>
<Button variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
<Button variant="standard" onClick={() => setCreating(false)}>{t('common_cancel')}</Button>
</div>
)}
</DetailPopup>
<DetailPopup open={!!selected} title={selected ? `${selected.code} — ${selected.name}` : ''} onClose={() => setSelected(null)} width={720} height={680}>
{selected && (
@@ -11,6 +11,7 @@ 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); }
@@ -42,6 +43,7 @@ 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]);
@@ -75,6 +77,7 @@ export default function ProjectInfoTab({ project, onChanged }) {
await runAction(async () => {
await addProjectMember(project.id, Number(addingMember));
setAddingMember('');
setAddMemberOpen(false);
onChanged();
});
}
@@ -185,20 +188,28 @@ export default function ProjectInfoTab({ project, onChanged }) {
)}
<div className="panel-card">
<h3>{t('project_members')}</h3>
<div className="toolbar">
<h3 style={{ margin: 0 }}>{t('project_members')}</h3>
<Button variant="standard" 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>
))}
<div style={{ display: 'flex', gap: 8, marginTop: 10 }}>
<div style={{ flex: 1 }}>
<ComboBox options={userOptions} value={addingMember} onChange={setAddingMember} placeholder={t('project_add_member')} />
</div>
<Button variant="standard" icon={UserPlus} onClick={addMember}>{t('project_add_member')}</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>
);
}