import { SearchableSelect } from '../components/SearchableSelect'; import { useEffect, useMemo, useState } from 'react'; import type { FormEvent } from 'react'; import { Link, useNavigate, useParams } from 'react-router'; import { useAuth } from '../auth/AuthContext'; import { Alert, EmptyState, LoadingBlock, errorMessage } from '../components/Feedback'; import { Icon } from '../components/Icon'; import { SURVEY_TARGET_STATUSES, surveyCampaignStatusLabel, surveyStatusClass, surveyTargetStatusLabel, } from '../features/surveys/surveyPresentation'; import { addSurveyTarget, assignSurveyTarget, createSurveyCampaign, getSurveyCampaign, listAssets, listSurveyAssignees, updateSurveyCampaign, updateSurveyCampaignStatus, updateSurveyTarget, updateSurveyTargetStatus, } from '../lib/api'; import type { AssetListItem, SurveyCampaign, SurveyCampaignStatus, SurveyPerson, SurveyTarget, SurveyTargetStatus, } from '../lib/api'; import { formatDate } from '../lib/format'; interface CampaignForm { code: string; name: string; description: string; plannedStartAt: string; plannedEndAt: string; scopeAssetId: string; coordinatorUserId: string; } interface TargetDraft { dueAt: string; instructions: string } const emptyCampaign: CampaignForm = { code: '', name: '', description: '', plannedStartAt: '', plannedEndAt: '', scopeAssetId: '', coordinatorUserId: '', }; function localDateTime(value: string | null): string { if (!value) return ''; const date = new Date(value); if (Number.isNaN(date.getTime())) return ''; const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000); return local.toISOString().slice(0, 16); } function isoOrNull(value: string): string | null { return value ? new Date(value).toISOString() : null; } function targetDrafts(targets: SurveyTarget[]): Record { return Object.fromEntries(targets.map((target) => [target.id, { dueAt: localDateTime(target.dueAt), instructions: target.instructions ?? '', }])); } function personName(person: SurveyPerson): string { return `${person.firstName} ${person.lastName}`; } export function SurveyCampaignEditorPage() { const { id } = useParams(); const navigate = useNavigate(); const { user, hasPermission } = useAuth(); const isNew = !id; const canManage = hasPermission('surveys.manage'); const canAssign = hasPermission('surveys.assign'); const canExecute = hasPermission('surveys.execute'); const canReadReports = hasPermission('surveys.read_reports'); const [campaign, setCampaign] = useState(null); const [form, setForm] = useState(emptyCampaign); const [assets, setAssets] = useState([]); const [assignees, setAssignees] = useState([]); const [assetSearch, setAssetSearch] = useState(''); const [targetAssetId, setTargetAssetId] = useState(''); const [targetAssigneeId, setTargetAssigneeId] = useState(''); const [targetDueAt, setTargetDueAt] = useState(''); const [targetInstructions, setTargetInstructions] = useState(''); const [drafts, setDrafts] = useState>({}); const [loading, setLoading] = useState(!isNew); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const applyCampaign = (value: SurveyCampaign, syncForm = true) => { setCampaign(value); setDrafts(targetDrafts(value.targets)); if (syncForm) setForm({ code: value.code, name: value.name, description: value.description ?? '', plannedStartAt: localDateTime(value.plannedStartAt), plannedEndAt: localDateTime(value.plannedEndAt), scopeAssetId: value.scopeAsset?.id ?? '', coordinatorUserId: value.coordinator?.id ?? '', }); }; useEffect(() => { if (!id) return; setLoading(true); getSurveyCampaign(id) .then((value) => applyCampaign(value)) .catch((requestError) => setError(errorMessage(requestError))) .finally(() => setLoading(false)); }, [id]); useEffect(() => { const timer = window.setTimeout(() => { listAssets({ pageSize: 100, search: assetSearch.trim() }) .then((response) => setAssets(response.data)) .catch(() => undefined); }, 220); return () => window.clearTimeout(timer); }, [assetSearch]); useEffect(() => { if (!canAssign) return; listSurveyAssignees().then(setAssignees).catch(() => undefined); }, [canAssign]); const closed = campaign?.status === 'COMPLETED' || campaign?.status === 'CANCELLED'; const targetIds = useMemo(() => new Set(campaign?.targets.map((target) => target.asset.id) ?? []), [campaign]); const candidateAssets = assets.filter((asset) => !targetIds.has(asset.id)); const saveCampaign = async (event: FormEvent) => { event.preventDefault(); setBusy(true); setError(''); setSuccess(''); const input = { code: form.code, name: form.name, description: form.description || null, plannedStartAt: isoOrNull(form.plannedStartAt), plannedEndAt: isoOrNull(form.plannedEndAt), scopeAssetId: form.scopeAssetId || null, coordinatorUserId: form.coordinatorUserId || null, }; try { if (isNew) { const created = await createSurveyCampaign(input); navigate(`/relevamiento/${created.id}`, { replace: true }); } else if (id) { applyCampaign(await updateSurveyCampaign(id, input)); setSuccess('Planificación actualizada.'); } } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const changeCampaignStatus = async (status: SurveyCampaignStatus) => { if (!id) return; setBusy(true); setError(''); setSuccess(''); try { applyCampaign(await updateSurveyCampaignStatus(id, status)); setSuccess(`Campaña ${surveyCampaignStatusLabel(status).toLocaleLowerCase('es-AR')}.`); } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const addTarget = async (event: FormEvent) => { event.preventDefault(); if (!id || !targetAssetId) return; setBusy(true); setError(''); setSuccess(''); try { applyCampaign(await addSurveyTarget(id, { assetId: targetAssetId, assignedUserId: targetAssigneeId || null, dueAt: isoOrNull(targetDueAt), instructions: targetInstructions || null, }), false); setTargetAssetId(''); setTargetAssigneeId(''); setTargetDueAt(''); setTargetInstructions(''); setSuccess('Registro agregado a la campaña.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const saveTargetPlan = async (target: SurveyTarget) => { const draft = drafts[target.id]; if (!draft) return; setBusy(true); setError(''); setSuccess(''); try { applyCampaign(await updateSurveyTarget(target.id, { dueAt: isoOrNull(draft.dueAt), instructions: draft.instructions || null, }), false); setSuccess(`Plan de ${target.asset.code} actualizado.`); } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const assignTarget = async (target: SurveyTarget, assignedUserId: string) => { setBusy(true); setError(''); setSuccess(''); try { applyCampaign(await assignSurveyTarget(target.id, assignedUserId || null), false); setSuccess(`Responsable de ${target.asset.code} actualizado.`); } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const changeTargetStatus = async (target: SurveyTarget, status: SurveyTargetStatus) => { setBusy(true); setError(''); setSuccess(''); try { applyCampaign(await updateSurveyTargetStatus(target.id, status), false); setSuccess(`Avance de ${target.asset.code} actualizado.`); } catch (requestError) { setError(errorMessage(requestError)); } finally { setBusy(false); } }; const campaignActions = campaign?.status === 'DRAFT' ? [{ status: 'PLANNED' as const, label: 'Confirmar planificación' }, { status: 'CANCELLED' as const, label: 'Cancelar' }] : campaign?.status === 'PLANNED' ? [{ status: 'IN_PROGRESS' as const, label: 'Iniciar campaña' }, { status: 'DRAFT' as const, label: 'Volver a borrador' }, { status: 'CANCELLED' as const, label: 'Cancelar' }] : campaign?.status === 'IN_PROGRESS' ? [{ status: 'COMPLETED' as const, label: 'Completar campaña' }, { status: 'CANCELLED' as const, label: 'Cancelar' }] : []; if (loading) return ; return
Relevamientos/{isNew ? 'Nueva campaña' : campaign?.code ?? 'Detalle'}
PLANIFICACIÓN DE CAMPO

{isNew ? 'Nueva campaña' : campaign?.name ?? 'Campaña'}

{isNew ? 'Definí alcance, fechas y coordinación inicial.' : `${campaign?.code} · ${campaign?.targetCount ?? 0} objetivos`}

{campaign && {surveyCampaignStatusLabel(campaign.status)}}
{error && {error}} {success && {success}}
DATOS GENERALES

Plan de relevamiento

{campaign && Actualizado {formatDate(campaign.updatedAt)}}