F4: remove obsolete generic-status inspection editor

This commit is contained in:
2026-09-07 23:10:48 -03:00
parent 7ac42fa05d
commit 5996c3819a
@@ -1,410 +0,0 @@
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 { useOperationalContext } from '../context/OperationalContext';
import {
inspectionStatusClass,
inspectionVisitStatusLabel,
} from '../features/inspections/inspectionPresentation';
import { InspectionActsPanel } from '../features/inspections/InspectionActsPanel';
import {
createInspectionVisit,
excludeInspectionVisitAsset,
generateInspectionVisitChecklist,
getInspectionVisit,
includeInspectionVisitAsset,
listAssets,
listInspectionAssignees,
listInspectionPlanningAreas,
listInspectionPlanningOperators,
replaceInspectionVisitAssets,
replaceInspectionVisitTeam,
updateInspectionVisit,
updateInspectionVisitStatus,
} from '../lib/api';
import type {
AssetListItem,
InspectionChecklistItemKind,
InspectionPerson,
InspectionPlanningContextAsset,
InspectionVisit,
InspectionVisitAssetPlanningSource,
InspectionVisitStatus,
} from '../lib/api';
import { formatDate, formatDateOnly } from '../lib/format';
interface VisitForm {
objective: string;
operationalAreaId: string;
operatorCompanyId: string;
plannedStartAt: string;
instructions: string;
}
const emptyVisit: VisitForm = {
objective: '',
operationalAreaId: '',
operatorCompanyId: '',
plannedStartAt: '',
instructions: '',
};
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 personName(person: InspectionPerson): string {
return `${person.firstName} ${person.lastName}`;
}
function sourceLabel(source: InspectionVisitAssetPlanningSource): string {
if (source === 'AUTOMATIC') return 'Checklist automático';
if (source === 'PREVENTIVE') return 'Preventivo';
if (source === 'VERIFICATION') return 'Verificación';
return 'Plan previo';
}
function checklistLabel(kind: InspectionChecklistItemKind): string {
if (kind === 'COMPANY_OVERDUE') return 'Respuesta vencida';
if (kind === 'VERIFICATION_OVERDUE') return 'Control vencido';
if (kind === 'UPCOMING_CONTROL') return 'Próximo control';
return 'Antecedente';
}
export function InspectionVisitEditorPage() {
const { id } = useParams();
const navigate = useNavigate();
const { hasPermission } = useAuth();
const operationalContext = useOperationalContext();
const isNew = !id;
const canManage = hasPermission('inspections.manage');
const canAssign = hasPermission('inspections.assign');
const [visit, setVisit] = useState<InspectionVisit | null>(null);
const [form, setForm] = useState<VisitForm>(emptyVisit);
const [areas, setAreas] = useState<InspectionPlanningContextAsset[]>([]);
const [operators, setOperators] = useState<InspectionPlanningContextAsset[]>([]);
const [assets, setAssets] = useState<AssetListItem[]>([]);
const [assignees, setAssignees] = useState<InspectionPerson[]>([]);
const [assetSearch, setAssetSearch] = useState('');
const [newAssetId, setNewAssetId] = useState('');
const [leadInspectorId, setLeadInspectorId] = useState('');
const [memberIds, setMemberIds] = useState<Set<string>>(new Set());
const [loading, setLoading] = useState(!isNew);
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const applyVisit = (value: InspectionVisit, syncGeneral = true) => {
setVisit(value);
if (value.operationalArea?.id) operationalContext.setContext(value.operationalArea.id, value.operatorCompany?.id ?? '');
setLeadInspectorId(value.leadInspector?.id ?? '');
setMemberIds(new Set(value.team.map((member) => member.id)));
if (syncGeneral) {
setForm({
objective: value.objective ?? '',
operationalAreaId: value.operationalArea?.id ?? '',
operatorCompanyId: value.operatorCompany?.id ?? '',
plannedStartAt: localDateTime(value.plannedStartAt),
instructions: value.instructions ?? '',
});
}
};
useEffect(() => {
listInspectionPlanningAreas().then(setAreas).catch(() => undefined);
}, []);
useEffect(() => {
if (!isNew || form.operationalAreaId || !operationalContext.areaId) return;
setForm((current) => ({
...current,
operationalAreaId: operationalContext.areaId,
operatorCompanyId: operationalContext.companyId,
}));
}, [isNew, form.operationalAreaId, operationalContext.areaId, operationalContext.companyId]);
useEffect(() => {
if (!id) return;
setLoading(true);
getInspectionVisit(id)
.then((value) => applyVisit(value))
.catch((requestError) => setError(errorMessage(requestError)))
.finally(() => setLoading(false));
}, [id]);
useEffect(() => {
if (!form.operationalAreaId) {
setOperators([]);
return;
}
listInspectionPlanningOperators(form.operationalAreaId)
.then(setOperators)
.catch(() => setOperators([]));
}, [form.operationalAreaId]);
useEffect(() => {
if (!form.operationalAreaId || !form.operatorCompanyId) {
setAssets([]);
return;
}
const timer = window.setTimeout(() => {
listAssets({
pageSize: 100,
search: assetSearch.trim(),
operationalAreaId: form.operationalAreaId,
operatorCompanyId: form.operatorCompanyId,
})
.then((response) => setAssets(response.data))
.catch(() => undefined);
}, 220);
return () => window.clearTimeout(timer);
}, [assetSearch, form.operationalAreaId, form.operatorCompanyId]);
useEffect(() => {
if (!canAssign) return;
listInspectionAssignees().then(setAssignees).catch(() => undefined);
}, [canAssign]);
const planningEditable = !visit || visit.status === 'DRAFT' || visit.status === 'PLANNED';
const activeAssetIds = useMemo(
() => new Set(visit?.assets.map((asset) => asset.id) ?? []),
[visit],
);
const linkedAssetIds = useMemo(
() => new Set(visit?.planningAssets.map((asset) => asset.id) ?? []),
[visit],
);
const candidateAssets = assets.filter((asset) => !linkedAssetIds.has(asset.id));
const saveGeneral = async (event: FormEvent) => {
event.preventDefault();
setBusy(true);
setError('');
setSuccess('');
try {
if (isNew) {
const plannedStartAt = isoOrNull(form.plannedStartAt);
if (!form.operationalAreaId || !form.operatorCompanyId || !plannedStartAt || !leadInspectorId) {
setError('Seleccioná Área, Operadora, fecha de inicio e Inspector.');
return;
}
const created = await createInspectionVisit({
operationalAreaId: form.operationalAreaId,
operatorCompanyId: form.operatorCompanyId,
plannedStartAt,
leadInspectorUserId: leadInspectorId,
});
navigate(`/inspecciones/${created.id}`, { replace: true });
} else if (id) {
applyVisit(await updateInspectionVisit(id, {
objective: form.objective || null,
operationalAreaId: form.operationalAreaId || null,
operatorCompanyId: form.operatorCompanyId || null,
plannedStartAt: isoOrNull(form.plannedStartAt),
instructions: form.instructions || null,
}));
setSuccess('Planificación actualizada. Si cambió el contexto o la fecha, regenerá el checklist.');
}
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setBusy(false);
}
};
const generateChecklist = async () => {
if (!id) return;
setBusy(true);
setError('');
setSuccess('');
try {
applyVisit(await generateInspectionVisitChecklist(id), false);
setSuccess('Checklist recalculado con antecedentes, vencidos y próximos controles.');
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setBusy(false);
}
};
const addPreventiveAsset = async (event: FormEvent) => {
event.preventDefault();
if (!id || !visit || !newAssetId) return;
setBusy(true);
setError('');
setSuccess('');
try {
applyVisit(await replaceInspectionVisitAssets(id, [...activeAssetIds, newAssetId]), false);
setNewAssetId('');
setSuccess('Registro agregado como inspección preventiva.');
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setBusy(false);
}
};
const excludeAsset = async (assetId: string) => {
if (!id) return;
const reason = window.prompt('Motivo de exclusión (mínimo 10 caracteres). Quedará auditado:')?.trim() || '';
if (reason.length < 10) {
if (reason) setError('El motivo de exclusión debe tener al menos 10 caracteres.');
return;
}
setBusy(true);
setError('');
setSuccess('');
try {
applyVisit(await excludeInspectionVisitAsset(id, assetId, reason), false);
setSuccess('Registro excluido con motivo auditado.');
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setBusy(false);
}
};
const reincludeAsset = async (assetId: string) => {
if (!id) return;
setBusy(true);
setError('');
setSuccess('');
try {
applyVisit(await includeInspectionVisitAsset(id, assetId), false);
setSuccess('Registro reincorporado; la exclusión anterior permanece en auditoría.');
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setBusy(false);
}
};
const chooseLead = (value: string) => {
setLeadInspectorId(value);
if (value) setMemberIds((current) => new Set([...current, value]));
};
const saveTeam = async (event: FormEvent) => {
event.preventDefault();
if (!id) return;
setBusy(true);
setError('');
setSuccess('');
try {
const nextMembers = new Set(memberIds);
if (leadInspectorId) nextMembers.add(leadInspectorId);
applyVisit(await replaceInspectionVisitTeam(
id,
leadInspectorId || null,
[...nextMembers],
), false);
setSuccess('Equipo de inspección actualizado.');
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setBusy(false);
}
};
const changeStatus = async (status: InspectionVisitStatus) => {
if (!id) return;
let reason: string | null = null;
if (status === 'CANCELLED') {
reason = window.prompt('Indicá el motivo de cancelación (mínimo 10 caracteres):')?.trim() || null;
if (!reason) return;
}
setBusy(true);
setError('');
setSuccess('');
try {
applyVisit(await updateInspectionVisitStatus(id, status, reason));
setSuccess(`Visita ${inspectionVisitStatusLabel(status).toLocaleLowerCase('es-AR')}.`);
} catch (requestError) {
setError(errorMessage(requestError));
} finally {
setBusy(false);
}
};
if (loading) return <LoadingBlock label="Cargando inspección…" />;
return <section className="survey-editor inspection-editor">
<div className="breadcrumb"><Link to="/inspecciones">Inspecciones</Link><span>/</span><span>{isNew ? 'Planificar inspección' : visit?.code ?? 'Detalle'}</span></div>
<div className="page-heading survey-editor-heading"><div><span className="eyebrow">INSPECCIÓN</span><h1>{isNew ? 'Planificar inspección' : visit?.code ?? 'Inspección'}</h1><p>{isNew ? 'Cuatro datos y listo. El código y el checklist se generan automáticamente.' : `${visit?.operationalArea?.name ?? 'Sin Área'} · ${visit?.operatorCompany?.name ?? 'Sin Operadora'} · ${visit?.assetCount ?? 0} registros`}</p></div>{visit && <span className={`status-badge large ${inspectionStatusClass(visit.status)}`}>{inspectionVisitStatusLabel(visit.status)}</span>}</div>
{error && <Alert>{error}</Alert>}{success && <Alert type="success">{success}</Alert>}
<form className={`panel form-panel ${isNew ? 'inspection-quick-create' : ''}`} onSubmit={saveGeneral}>
<div className="panel-heading"><div><span className="eyebrow">{isNew ? 'CREACIÓN RÁPIDA' : 'PLANIFICACIÓN'}</span><h2>{isNew ? '¿Dónde y cuándo se inspecciona?' : 'Contexto y fecha de inicio'}</h2><p className="section-copy">{isNew ? 'El código se asigna automáticamente. No hay título ni fecha de fin planificada.' : 'La inspección conserva una única fecha de inicio planificada; el cierre real se registra al finalizar en campo.'}</p></div>{visit && <small className="muted">Actualizado {formatDate(visit.updatedAt)}</small>}</div>
{!isNew && <div className="inspection-generated-code"><small>Código automático</small><strong>{visit?.code}</strong></div>}
<div className="form-grid">
<label className="field"><span>Área</span><SearchableSelect searchPlaceholder="Buscar Área…" value={form.operationalAreaId} onChange={(event) => { operationalContext.setAreaId(event.target.value); setForm((current) => ({ ...current, operationalAreaId: event.target.value, operatorCompanyId: '' })); }} required disabled={!canManage || !planningEditable}><option value="">Seleccionar Área</option>{visit?.operationalArea && !areas.some((area) => area.id === visit.operationalArea?.id) && <option value={visit.operationalArea.id}>{visit.operationalArea.name} · {visit.operationalArea.code}</option>}{areas.map((area) => <option key={area.id} value={area.id}>{area.name} · {area.code}</option>)}</SearchableSelect></label>
<label className="field"><span>Operadora</span><SearchableSelect searchPlaceholder="Buscar Operadora…" value={form.operatorCompanyId} onChange={(event) => { operationalContext.setCompanyId(event.target.value); setForm((current) => ({ ...current, operatorCompanyId: event.target.value })); }} required disabled={!canManage || !planningEditable || !form.operationalAreaId}><option value="">{form.operationalAreaId ? 'Seleccionar Operadora…' : 'Primero seleccioná un Área'}</option>{visit?.operatorCompany && !operators.some((operator) => operator.id === visit.operatorCompany?.id) && <option value={visit.operatorCompany.id}>{visit.operatorCompany.name} · {visit.operatorCompany.code}</option>}{operators.map((operator) => <option key={operator.id} value={operator.id}>{operator.name} · {operator.code}</option>)}</SearchableSelect><small>Sólo operadoras vigentes para el Área elegida.</small></label>
<label className="field"><span>Fecha y hora de inicio</span><input type="datetime-local" value={form.plannedStartAt} onChange={(event) => setForm((current) => ({ ...current, plannedStartAt: event.target.value }))} required disabled={!canManage || !planningEditable} /></label>
{isNew && <label className="field"><span>Inspector</span><SearchableSelect searchPlaceholder="Buscar Inspector…" value={leadInspectorId} onChange={(event) => setLeadInspectorId(event.target.value)} required disabled={!canAssign}><option value="">Seleccionar Inspector</option>{assignees.map((person) => <option key={person.id} value={person.id}>{personName(person)} · {person.username}</option>)}</SearchableSelect></label>}
</div>
{!isNew && <><label className="field"><span>Objetivo <em>opcional</em></span><textarea rows={2} value={form.objective} onChange={(event) => setForm((current) => ({ ...current, objective: event.target.value }))} maxLength={4000} disabled={!canManage || !planningEditable} /></label><label className="field"><span>Instrucciones <em>opcional</em></span><textarea rows={2} value={form.instructions} onChange={(event) => setForm((current) => ({ ...current, instructions: event.target.value }))} maxLength={4000} disabled={!canManage || !planningEditable} /></label></>}
{canManage && planningEditable && <div className="form-actions"><Link className="button secondary" to="/inspecciones">Cancelar</Link><button className="button primary" disabled={busy || (isNew && (!canAssign || !form.operationalAreaId || !form.operatorCompanyId || !form.plannedStartAt || !leadInspectorId))}><Icon name="check" />{isNew ? (busy ? 'Creando…' : 'Crear inspección') : 'Guardar planificación'}</button></div>}
</form>
{visit && <article className="panel inspection-checklist-panel">
<div className="panel-heading"><div><span className="eyebrow">CHECKLIST AUTOMÁTICO</span><h2>Antecedentes y próximas acciones</h2><p className="section-copy">Se calcula para el Área, Operadora y fecha del plan. Las generaciones anteriores quedan preservadas.</p></div><div className="form-actions">{canManage && planningEditable && <button type="button" className="button secondary" onClick={() => void generateChecklist()} disabled={busy}><Icon name="check" />{visit.checklist.generation ? 'Regenerar checklist' : 'Generar checklist'}</button>}</div></div>
{visit.checklist.stale && <Alert>El contexto o la fecha cambió después de la última generación. Regenerá el checklist antes de confirmar.</Alert>}
{!visit.checklist.generatedAt && visit.checklist.generation === 0 && <Alert>El checklist todavía no fue generado. Al confirmar la planificación el servidor también exige una versión vigente.</Alert>}
<div className="inspection-checklist-metrics">
<div><strong>{visit.checklist.companyOverdue}</strong><span>respuestas vencidas</span></div>
<div><strong>{visit.checklist.verificationOverdue}</strong><span>controles vencidos</span></div>
<div><strong>{visit.checklist.upcomingControls}</strong><span>próximos 30 días</span></div>
<div><strong>{visit.checklist.antecedents}</strong><span>antecedentes</span></div>
<div><strong>{visit.checklist.actionableAssets}</strong><span>registros sugeridos</span></div>
</div>
{visit.checklist.generatedAt && <small className="muted">Generación {visit.checklist.generation} · {formatDate(visit.checklist.generatedAt)}</small>}
{visit.checklist.items.length === 0 ? <EmptyState title="Sin antecedentes" text="No hay hallazgos históricos para el contexto seleccionado. Podés agregar registros preventivos." /> : <div className="table-scroll"><table><thead><tr><th>Prioridad</th><th>Hallazgo</th><th>Registro</th><th>Fecha</th><th>Gravedad</th></tr></thead><tbody>{visit.checklist.items.map((item) => <tr key={item.id}><td><span className={`status-badge ${item.itemKind === 'ANTECEDENT' ? '' : 'warning'}`}>{checklistLabel(item.itemKind)}</span></td><td><Link className="history-asset-link" to={`/hallazgos/${item.findingId}`}><strong>{item.findingTitle}</strong><small>{item.findingCode} · {item.findingStatus}</small></Link></td><td><Link className="history-asset-link" to={`/inventarios/${item.asset.id}`}><strong>{item.asset.name}</strong><small>{item.asset.code} · {item.asset.typeName}</small></Link></td><td>{item.referenceOn ? formatDateOnly(item.referenceOn) : '—'}</td><td>{item.severity ?? '—'}</td></tr>)}</tbody></table></div>}
</article>}
{visit && visit.verificationFindings.length > 0 && <article className="panel verification-visit-findings">
<div className="panel-heading"><div><span className="eyebrow">VERIFICACIÓN DE HALLAZGOS</span><h2>Hallazgos a controlar</h2><p className="section-copy">Esta inspección nació desde una planificación de verificación y conserva ese vínculo histórico.</p></div><span className="count-pill">{visit.verificationFindings.length}</span></div>
<div className="dossier-link-list">{visit.verificationFindings.map((finding) => {
const outcome = finding.outcome === 'RESOLVED' ? 'Solucionado' : finding.outcome === 'NOT_RESOLVED' ? 'No solucionado' : finding.outcome === 'REQUIRES_NEW_DATE' ? 'Nueva fecha requerida' : visit.status === 'IN_PROGRESS' ? 'Pendiente de resultado' : 'Abrir seguimiento';
return <Link key={finding.id} to={`/hallazgos/${finding.id}`}><div><strong>{finding.title}</strong><small>{finding.code} · {finding.assetName} · objetivo {formatDateOnly(finding.targetControlOn ?? finding.nextControlOn)}{finding.verificationEvidenceCount ? ` · ${finding.verificationEvidenceCount} foto${finding.verificationEvidenceCount === 1 ? '' : 's'}` : ''}</small>{finding.resultNotes && <small>{finding.resultNotes}</small>}{finding.rescheduledControlOn && <small>Nuevo control: {formatDateOnly(finding.rescheduledControlOn)}</small>}</div><span>{finding.status === 'CLOSED' ? 'Cerrado' : outcome}</span><Icon name="chevron" size={16} /></Link>;
})}</div>
</article>}
{visit && planningEditable && canManage && <form className="panel survey-add-target" onSubmit={addPreventiveAsset}>
<div className="panel-heading"><div><span className="eyebrow">PREVENTIVO</span><h2>Agregar registro sin pendiente previo</h2><p className="section-copy">Sólo se muestran registros pertenecientes al Área y Operadora seleccionadas.</p></div></div>
<label className="field"><span>Buscar registro</span><input value={assetSearch} onChange={(event) => setAssetSearch(event.target.value)} placeholder="Código o nombre" /></label>
<div className="form-grid"><label className="field"><span>Registro preventivo</span><SearchableSelect value={newAssetId} onChange={(event) => setNewAssetId(event.target.value)} required><option value="">Seleccionar</option>{candidateAssets.map((asset) => <option key={asset.id} value={asset.id}>{asset.code} · {asset.name} · {asset.type.name}</option>)}</SearchableSelect></label></div>
<div className="form-actions"><button className="button primary" disabled={busy || !newAssetId}><Icon name="plus" />Agregar preventivo</button></div>
</form>}
{visit && <div className="table-panel inspection-assets"><div className="table-summary"><strong>{visit.assets.length} registro{visit.assets.length === 1 ? '' : 's'} incluidos</strong><span>{visit.checklist.excludedAssets} excluidos con trazabilidad.</span></div>{visit.planningAssets.length === 0 ? <EmptyState title="Sin registros" text="Generá el checklist o agregá al menos un registro preventivo." /> : <div className="table-scroll"><table><thead><tr><th>Registro</th><th>Origen</th><th>Estado</th><th>Motivo</th><th /></tr></thead><tbody>{visit.planningAssets.map((asset) => <tr key={asset.id}><td><Link className="history-asset-link" to={`/inventarios/${asset.id}`}><strong>{asset.name}</strong><small>{asset.code} · {asset.typeName}</small></Link></td><td>{sourceLabel(asset.planningSource)}</td><td><span className={`status-badge ${asset.included ? 'success' : 'muted'}`}>{asset.included ? 'Incluido' : 'Excluido'}</span></td><td>{asset.exclusionReason ? <><strong>{asset.exclusionReason}</strong><small>{asset.excludedBy ? `Por ${personName(asset.excludedBy)}` : ''}{asset.excludedAt ? ` · ${formatDate(asset.excludedAt)}` : ''}</small></> : '—'}</td><td className="action-cell">{canManage && planningEditable && (asset.included ? <button type="button" className="button danger-outline compact" onClick={() => void excludeAsset(asset.id)} disabled={busy}>Excluir</button> : <button type="button" className="button secondary compact" onClick={() => void reincludeAsset(asset.id)} disabled={busy}>Reincorporar</button>)}</td></tr>)}</tbody></table></div>}</div>}
{visit && canAssign && planningEditable && <form className="panel inspection-team-panel" onSubmit={saveTeam}>
<div className="panel-heading"><div><span className="eyebrow">EQUIPO</span><h2>Responsable e integrantes</h2></div><span className="count-pill">{memberIds.size}</span></div>
<label className="field"><span>Inspector responsable</span><SearchableSelect value={leadInspectorId} onChange={(event) => chooseLead(event.target.value)}><option value="">Seleccionar</option>{assignees.map((person) => <option key={person.id} value={person.id}>{personName(person)} · {person.username}</option>)}</SearchableSelect></label>
<div className="inspection-team-grid">{assignees.map((person) => <label className={`inspection-member ${memberIds.has(person.id) ? 'selected' : ''}`} key={person.id}><input type="checkbox" checked={memberIds.has(person.id)} disabled={person.id === leadInspectorId} onChange={(event) => setMemberIds((current) => { const next = new Set(current); event.target.checked ? next.add(person.id) : next.delete(person.id); return next; })} /><span><strong>{personName(person)}</strong><small>{person.username}{person.id === leadInspectorId ? ' · Responsable' : ''}</small></span></label>)}</div>
<div className="form-actions"><button className="button primary" disabled={busy}><Icon name="check" />Guardar equipo</button></div>
</form>}
{visit && !canAssign && <article className="panel"><div className="panel-heading"><div><span className="eyebrow">EQUIPO</span><h2>{visit.leadInspector ? personName(visit.leadInspector) : 'Sin responsable'}</h2></div><span className="count-pill">{visit.team.length}</span></div><div className="inspection-team-grid">{visit.team.map((member) => <div className="inspection-member selected" key={member.id}><span><strong>{personName(member)}</strong><small>{member.username}{member.id === visit.leadInspector?.id ? ' · Responsable' : ''}</small></span></div>)}</div></article>}
{visit && <InspectionActsPanel visit={visit} />}
{visit && <div className="survey-status-actions panel"><div><strong>Flujo de la inspección</strong><p>Oficina planifica y asigna. El inspector responsable inicia la inspección desde la APK; toda la ejecución de campo sigue siendo exclusiva del dispositivo móvil.</p></div><div>{canManage && visit.status === 'DRAFT' && <button type="button" className="button secondary" onClick={() => void changeStatus('PLANNED')} disabled={busy}>Confirmar planificación</button>}{canManage && visit.status === 'PLANNED' && <button type="button" className="button secondary" onClick={() => void changeStatus('DRAFT')} disabled={busy}>Volver a borrador</button>}{canManage && ['DRAFT', 'PLANNED'].includes(visit.status) && <button type="button" className="button danger-outline" onClick={() => void changeStatus('CANCELLED')} disabled={busy}>Cancelar planificación</button>}</div></div>}
{visit?.status === 'PLANNED' && <div className="temporal-notice"><Icon name="clipboard" /><p><strong>Planificación lista.</strong> El inspector asignado debe iniciar la inspección desde la APK. El dashboard no dispone de acción de inicio.</p></div>}
{visit?.status === 'IN_PROGRESS' && <div className="temporal-notice"><Icon name="clipboard" /><p><strong>Inspección iniciada {formatDate(visit.actualStartedAt)}.</strong> Las actas, hallazgos, evidencias y el cierre de la inspección se registran desde la APK; aquí se consultan.</p></div>}
{visit?.status === 'CANCELLED' && <Alert>Cancelada: {visit.cancellationReason}</Alert>}
</section>;
}