import { useEffect, useRef, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { Plus, Save, Trash2, ArrowRightLeft, FolderUp, Archive, RotateCcw, Sparkles } 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, convertActivityToProject, listActivitiesTrash, restoreActivity } from '../../services/activities.js';
import { listProjectOptions } from '../../services/projects.js';
import { search as searchApi } from '../../services/search.js';
import { listCatalog } from '../../services/catalogs.js';
import { listUsers } from '../../services/users.js';
import { ListView } from '../../components/ListView.jsx';
import { SearchInput } from '../../components/SearchInput.jsx';
import { SearchResultsDropdown } from '../../components/SearchResultsDropdown.jsx';
import { Button } from '../../components/Button.jsx';
import { Field, FieldRow } from '../../components/Field.jsx';
import { ComboBox } from '../../components/ComboBox.jsx';
import { StatusBadge, ProgressBar, DueBadge } from '../../components/StatusBadge.jsx';
import { dueStatus } from '../../utils/dueStatus.js';
import { DetailPopup } from '../../components/DetailPopup.jsx';
import ActivityDetailPanel from './ActivityDetailPanel.jsx';
import { AiPlanDialog } from './AiPlanDialog.jsx';
import { getAiStatus } from '../../services/ai.js';
import { DraftNotice } from '../../components/DraftNotice.jsx';
import { DraftMark, useGuardedClose } from '../../components/NavigationGuard.jsx';
import { useDraftForm, useUserDrafts, hasDraftFor } from '../../hooks/useDraftForm.js';
const EMPTY_FORM = { name: '', description: '', responsibleUserId: '', priorityId: '', statusId: '', startDate: '', dueDate: '', isMilestone: false };
export default function ProjectActivitiesTab({ project, projectId }) {
const { t, pushToast, openDialog } = useUi();
const { user: currentUser } = useAuth();
const navigate = useNavigate();
const runAction = useAction();
const { data: activities, reload } = useAsync(() => listActivitiesByProject(projectId), [projectId]);
// Boton de IA solo si el superadmin configuro la clave; el backend valida permisos al usarlo.
const { data: aiStatus } = useAsync(() => getAiStatus().catch(() => ({ enabled: false })), []);
const [aiPlanOpen, setAiPlanOpen] = useState(false);
const [trashOpen, setTrashOpen] = useState(false);
const { data: trashed, reload: reloadTrash } = useAsync(
() => (trashOpen ? listActivitiesTrash(projectId) : Promise.resolve([])),
[trashOpen, projectId]
);
function restoreFromTrash(activity) {
runAction(async () => {
await restoreActivity(activity.id);
await reloadTrash();
reload();
pushToast({ tone: 'success', message: t('common_saved') });
});
}
const { data: priorities } = useAsync(() => listCatalog('priorities'), []);
const { data: statuses } = useAsync(() => listCatalog('activity-statuses'), []);
const { data: users } = useAsync(listUsers, []);
const { data: allProjects } = useAsync(listProjectOptions, []);
const [search, setSearch] = useState('');
const [creating, setCreating] = useState(false);
const [selected, setSelected] = useState(null);
const [moving, setMoving] = useState(null);
const [targetProjectId, setTargetProjectId] = useState('');
const [converting, setConverting] = useState(null);
const drafts = useUserDrafts();
const guardedClose = useGuardedClose();
// "Nueva actividad" con borrador (por proyecto); se reabre al volver si quedo a medio llenar.
const draft = useDraftForm({ formId: 'activity-new', entityId: projectId, initial: EMPTY_FORM, label: `${project.code} — ${t('activity_new')}`, enabled: creating });
const form = draft.values;
const setForm = draft.setValues;
const pendingNew = hasDraftFor(drafts, 'activity-new', projectId);
useEffect(() => { if (pendingNew) setCreating(true); }, []); // eslint-disable-line react-hooks/exhaustive-deps
// "Convertir en proyecto" con borrador (por actividad).
const convertDraft = useDraftForm({
formId: 'activity-convert', entityId: converting?.id,
initial: { projectName: converting?.name || '', taskActivityName: converting?.name || '' },
label: `${converting?.code || ''} — ${t('activity_convert_title')}`, enabled: !!converting,
});
const convertForm = convertDraft.values;
const setConvertForm = convertDraft.setValues;
// Actividad abierta y su pestaña viven en la URL (?activity=&activityTab=): F5, el retorno tras
// el login y el aviso de cambios sin guardar (al cerrar el popup o cambiar de pestaña) usan eso.
const [searchParams, setSearchParams] = useSearchParams();
const selectedId = searchParams.get('activity') ? Number(searchParams.get('activity')) : null;
const activityTab = searchParams.get('activityTab') || 'detail';
const setActivityParams = (id, tab) => setSearchParams((prev) => {
const next = new URLSearchParams(prev);
if (id) {
next.set('activity', id);
if (tab && tab !== 'detail') next.set('activityTab', tab); else next.delete('activityTab');
} else {
next.delete('activity');
next.delete('activityTab');
}
return next;
}, { replace: true });
useEffect(() => {
if (!selectedId) { setSelected(null); return undefined; }
let alive = true;
// Si la actividad ya no existe o no hay acceso, no se abre (ni se restauran sus borradores).
getActivity(selectedId)
.then((a) => { if (alive) setSelected(a); })
.catch(() => { if (alive) setActivityParams(null); });
return () => { alive = false; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedId]);
// 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.
const debouncedSearch = useDebouncedValue(search, 300);
const searchQuery = debouncedSearch.trim();
const { data: searchResults } = useAsync(
() => (searchQuery.length >= 2 ? searchApi(searchQuery, projectId) : Promise.resolve(null)),
[searchQuery, projectId]
);
const searchItems = searchResults ? [
...searchResults.activities.map((a) => ({
key: `a-${a.id}`,
breadcrumb: [`${a.code} — ${a.name}`],
onClick: () => { setSearch(''); openActivity({ id: a.id }); },
})),
...searchResults.tasks.map((tk) => ({
key: `t-${tk.id}`,
breadcrumb: [`${tk.activity_code} — ${tk.activity_name}`, tk.name],
onClick: () => { setSearch(''); openActivity({ id: tk.activity_id }, 'tasks'); },
})),
] : null;
// Al volver con un borrador de "Convertir en proyecto", se reabre sobre su actividad.
useEffect(() => {
if (!activities || converting) return;
const pending = drafts.find((d) => d.formId === 'activity-convert');
const row = pending && activities.find((a) => String(a.id) === String(pending.entityId));
if (row) setConverting(row);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activities]);
const priorityOptions = (priorities || []).map((p) => ({ value: p.id, label: p.name }));
const statusOptions = (statuses || []).map((s) => ({ value: s.id, label: s.name }));
const userOptions = (users || []).map((u) => ({ value: u.id, label: u.name }));
const projectOptions = (allProjects || [])
.filter((p) => p.id !== Number(projectId))
.map((p) => ({ value: p.id, label: `${p.code} — ${p.name}` }));
async function save() {
if (!form.name.trim() || !form.priorityId || !form.statusId) return;
await runAction(async () => {
await createActivity(projectId, { ...form, responsibleUserId: form.responsibleUserId || null });
draft.clear();
setCreating(false);
pushToast({ tone: 'success', message: t('common_saved') });
await reload();
});
}
function openActivity(row, initialTab) {
setActivityParams(row.id, initialTab);
}
async function refreshAll() {
await reload();
if (selected) setSelected(await getActivity(selected.id));
}
async function handleReorder(ids) {
await reorderActivities(projectId, ids);
await reload();
}
function startMove(row) {
setMoving(row);
setTargetProjectId('');
}
function confirmMove() {
const target = (allProjects || []).find((p) => p.id === Number(targetProjectId));
if (!target) return;
const message = t('activity_move_confirm_msg').replace('{activity}', moving.name).replace('{project}', target.name);
openDialog({
tone: 'warning',
title: t('activity_move_title'),
message,
actions: [
{ label: t('common_cancel'), variant: 'standard' },
{
label: t('common_accept'), variant: 'accent',
onClick: () => runAction(async () => {
await moveActivity(moving.id, Number(targetProjectId));
setMoving(null);
await reload();
pushToast({ tone: 'success', message: t('common_saved') });
}),
},
],
});
}
function startConvert(row) {
setConverting(row);
}
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());
convertDraft.clear();
setConverting(null);
pushToast({ tone: 'success', message: t('common_saved') });
navigate(`/proyectos/${result.project.id}`);
}),
},
],
});
}
function confirmDelete(row) {
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 deleteActivity(row.id);
await reload();
pushToast({ tone: 'success', message: t('common_saved') });
}),
},
],
});
}
const columns = [
{ key: 'code', label: t('activity_code') },
{ key: 'name', label: t('common_name') },
{ key: 'responsible_name', label: t('common_responsible'), render: (r) => r.responsible_name || '—' },
{ key: 'status_name', label: t('common_status'), render: (r) =>