feat: convertir una actividad en proyecto (con sus tareas)
Una tarea siempre necesita una actividad contenedora, asi que convertir crea DOS cosas: el proyecto nuevo y, dentro, una actividad nueva que recibe las tareas -- y comentarios/adjuntos -- de la actividad original. Se piden ambos nombres (precargados con el nombre actual) antes de confirmar. La actividad original se borra al final, ya vacia de tareas (no choca con el bloqueo de removeActivity). Exige el mismo permiso que crear un proyecto, no solo escribir la actividad. Nuevo POST /api/activities/:id/convert-to-project. Probado de punta a punta en dev: prioridad heredada, tareas y comentario movidos correctamente, actividad original eliminada, navega al proyecto nuevo al terminar. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Plus, Save, Trash2, ArrowRightLeft } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Plus, Save, Trash2, ArrowRightLeft, FolderUp } from 'lucide-react';
|
||||
import { useUi } from '../../context/ui-context.jsx';
|
||||
import { useAuth } from '../../context/auth-context.jsx';
|
||||
import { useAsync, useAction } from '../../hooks/useAsync.js';
|
||||
import { useDebouncedValue } from '../../hooks/useDebouncedValue.js';
|
||||
import { listActivitiesByProject, createActivity, getActivity, deleteActivity, reorderActivities, moveActivity } from '../../services/activities.js';
|
||||
import { listActivitiesByProject, createActivity, getActivity, deleteActivity, reorderActivities, moveActivity, convertActivityToProject } from '../../services/activities.js';
|
||||
import { listProjects } from '../../services/projects.js';
|
||||
import { search as searchApi } from '../../services/search.js';
|
||||
import { listCatalog } from '../../services/catalogs.js';
|
||||
@@ -25,6 +26,7 @@ const EMPTY_FORM = { name: '', description: '', responsibleUserId: '', priorityI
|
||||
export default function ProjectActivitiesTab({ projectId, openActivityId, openActivityTab }) {
|
||||
const { t, pushToast, openDialog } = useUi();
|
||||
const { user: currentUser } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const runAction = useAction();
|
||||
const { data: activities, reload } = useAsync(() => listActivitiesByProject(projectId), [projectId]);
|
||||
const { data: priorities } = useAsync(() => listCatalog('priorities'), []);
|
||||
@@ -39,6 +41,8 @@ export default function ProjectActivitiesTab({ projectId, openActivityId, openAc
|
||||
const [selectedInitialTab, setSelectedInitialTab] = useState(undefined);
|
||||
const [moving, setMoving] = useState(null);
|
||||
const [targetProjectId, setTargetProjectId] = useState('');
|
||||
const [converting, setConverting] = useState(null);
|
||||
const [convertForm, setConvertForm] = useState({ projectName: '', taskActivityName: '' });
|
||||
|
||||
// Busca tambien tareas de esta actividad (no solo actividades, que ya filtra la tabla mas abajo
|
||||
// via getSearchHaystack) — resultados con su ruta "Actividad - Tarea" en un panel flotante.
|
||||
@@ -132,6 +136,37 @@ export default function ProjectActivitiesTab({ projectId, openActivityId, openAc
|
||||
});
|
||||
}
|
||||
|
||||
function startConvert(row) {
|
||||
setConverting(row);
|
||||
setConvertForm({ projectName: row.name, taskActivityName: row.name });
|
||||
}
|
||||
|
||||
function confirmConvert() {
|
||||
const { projectName, taskActivityName } = convertForm;
|
||||
if (!projectName.trim() || !taskActivityName.trim()) return;
|
||||
const message = t('activity_convert_confirm_msg')
|
||||
.replace('{activity}', converting.name)
|
||||
.replace('{project}', projectName.trim())
|
||||
.replace('{newActivity}', taskActivityName.trim());
|
||||
openDialog({
|
||||
tone: 'warning',
|
||||
title: t('activity_convert_title'),
|
||||
message,
|
||||
actions: [
|
||||
{ label: t('common_cancel'), variant: 'standard' },
|
||||
{
|
||||
label: t('common_accept'), variant: 'accent',
|
||||
onClick: () => runAction(async () => {
|
||||
const result = await convertActivityToProject(converting.id, projectName.trim(), taskActivityName.trim());
|
||||
setConverting(null);
|
||||
pushToast({ tone: 'success', message: t('common_saved') });
|
||||
navigate(`/proyectos/${result.project.id}`);
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function confirmDelete(row) {
|
||||
openDialog({
|
||||
tone: 'warning',
|
||||
@@ -191,6 +226,7 @@ export default function ProjectActivitiesTab({ projectId, openActivityId, openAc
|
||||
onReorder={currentUser?.roleCode !== 'viewer' ? handleReorder : undefined}
|
||||
rowActions={currentUser?.roleCode !== 'viewer' ? (a) => [
|
||||
{ label: t('activity_move'), icon: ArrowRightLeft, onClick: () => startMove(a) },
|
||||
{ label: t('activity_convert'), icon: FolderUp, onClick: () => startConvert(a) },
|
||||
{ label: t('common_delete'), icon: Trash2, danger: true, onClick: () => confirmDelete(a) },
|
||||
] : undefined}
|
||||
/>
|
||||
@@ -209,6 +245,35 @@ export default function ProjectActivitiesTab({ projectId, openActivityId, openAc
|
||||
)}
|
||||
</DetailPopup>
|
||||
|
||||
<DetailPopup open={!!converting} title={t('activity_convert_title')} onClose={() => setConverting(null)} width={520} height={360}>
|
||||
{converting && (
|
||||
<>
|
||||
<div className="text-muted" style={{ fontSize: '.78rem', marginBottom: 12 }}>{t('activity_convert_hint')}</div>
|
||||
<Field label={t('activity_convert_project_name')}>
|
||||
<input
|
||||
className="input" value={convertForm.projectName} autoFocus
|
||||
onChange={(e) => setConvertForm((f) => ({ ...f, projectName: e.target.value }))}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t('activity_convert_activity_name')}>
|
||||
<input
|
||||
className="input" value={convertForm.taskActivityName}
|
||||
onChange={(e) => setConvertForm((f) => ({ ...f, taskActivityName: e.target.value }))}
|
||||
/>
|
||||
</Field>
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||||
<Button
|
||||
variant="accent" icon={FolderUp} onClick={confirmConvert}
|
||||
disabled={!convertForm.projectName.trim() || !convertForm.taskActivityName.trim()}
|
||||
>
|
||||
{t('activity_convert')}
|
||||
</Button>
|
||||
<Button variant="standard" onClick={() => setConverting(null)}>{t('common_cancel')}</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DetailPopup>
|
||||
|
||||
<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 />
|
||||
|
||||
Reference in New Issue
Block a user