F4: add clean inspection planning editor
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
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 { SearchableSelect } from '../components/SearchableSelect';
|
||||
import { useOperationalContext } from '../context/OperationalContext';
|
||||
import { InspectionActsPanel } from '../features/inspections/InspectionActsPanel';
|
||||
import {
|
||||
inspectionStatusClass,
|
||||
inspectionVisitStatusLabel,
|
||||
} from '../features/inspections/inspectionPresentation';
|
||||
import {
|
||||
createInspectionVisit,
|
||||
excludeInspectionVisitAsset,
|
||||
generateInspectionVisitChecklist,
|
||||
getInspectionVisit,
|
||||
includeInspectionVisitAsset,
|
||||
listAssets,
|
||||
listInspectionAssignees,
|
||||
listInspectionPlanningAreas,
|
||||
listInspectionPlanningOperators,
|
||||
replaceInspectionVisitAssets,
|
||||
replaceInspectionVisitTeam,
|
||||
updateInspectionVisit,
|
||||
} from '../lib/api';
|
||||
import type {
|
||||
AssetListItem,
|
||||
InspectionPerson,
|
||||
InspectionPlanningContextAsset,
|
||||
InspectionVisit,
|
||||
InspectionVisitAssetPlanningSource,
|
||||
} from '../lib/api';
|
||||
import {
|
||||
cancelInspectionVisit,
|
||||
planInspectionVisit,
|
||||
unplanInspectionVisit,
|
||||
} from '../lib/inspectionVisitLifecycleApi';
|
||||
import { formatDate, formatDateOnly } from '../lib/format';
|
||||
|
||||
interface VisitForm {
|
||||
objective: string;
|
||||
operationalAreaId: string;
|
||||
operatorCompanyId: string;
|
||||
plannedStartAt: string;
|
||||
instructions: string;
|
||||
}
|
||||
|
||||
const emptyForm: VisitForm = {
|
||||
objective: '',
|
||||
operationalAreaId: '',
|
||||
operatorCompanyId: '',
|
||||
plannedStartAt: '',
|
||||
instructions: '',
|
||||
};
|
||||
|
||||
function localDateTime(value: string | null): string {
|
||||
if (!value) return '';
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) return '';
|
||||
const local = new Date(parsed.getTime() - parsed.getTimezoneOffset() * 60_000);
|
||||
return local.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
function toIso(value: string): string | null {
|
||||
if (!value) return null;
|
||||
const parsed = new Date(value);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
||||
}
|
||||
|
||||
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: string): string {
|
||||
if (kind === 'VERIFICATION_OVERDUE') return 'Control vencido';
|
||||
if (kind === 'UPCOMING_CONTROL') return 'Próximo control';
|
||||
return 'Antecedente';
|
||||
}
|
||||
|
||||
export function InspectionVisitEditorF4Page() {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { hasPermission } = useAuth();
|
||||
const context = 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>(emptyForm);
|
||||
const [areas, setAreas] = useState<InspectionPlanningContextAsset[]>([]);
|
||||
const [operators, setOperators] = useState<InspectionPlanningContextAsset[]>([]);
|
||||
const [assignees, setAssignees] = useState<InspectionPerson[]>([]);
|
||||
const [assets, setAssets] = useState<AssetListItem[]>([]);
|
||||
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, syncForm = true) => {
|
||||
setVisit(value);
|
||||
setLeadInspectorId(value.leadInspector?.id ?? '');
|
||||
setMemberIds(new Set(value.team.map((member) => member.id)));
|
||||
if (value.operationalArea?.id) {
|
||||
context.setContext(value.operationalArea.id, value.operatorCompany?.id ?? '');
|
||||
}
|
||||
if (syncForm) {
|
||||
setForm({
|
||||
objective: value.objective ?? '',
|
||||
operationalAreaId: value.operationalArea?.id ?? '',
|
||||
operatorCompanyId: value.operatorCompany?.id ?? '',
|
||||
plannedStartAt: localDateTime(value.plannedStartAt),
|
||||
instructions: value.instructions ?? '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const reload = async () => {
|
||||
if (!id) return;
|
||||
applyVisit(await getInspectionVisit(id));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
listInspectionPlanningAreas().then(setAreas).catch(() => undefined);
|
||||
if (canAssign) listInspectionAssignees().then(setAssignees).catch(() => undefined);
|
||||
}, [canAssign]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNew || form.operationalAreaId || !context.areaId) return;
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
operationalAreaId: context.areaId,
|
||||
operatorCompanyId: context.companyId,
|
||||
}));
|
||||
}, [isNew, form.operationalAreaId, context.areaId, context.companyId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
reload()
|
||||
.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]);
|
||||
|
||||
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 {
|
||||
const plannedStartAt = toIso(form.plannedStartAt);
|
||||
if (isNew) {
|
||||
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.trim() || null,
|
||||
operationalAreaId: form.operationalAreaId || null,
|
||||
operatorCompanyId: form.operatorCompanyId || null,
|
||||
plannedStartAt,
|
||||
instructions: form.instructions.trim() || null,
|
||||
}));
|
||||
setSuccess('Planificación actualizada. Regenerá el checklist si cambió contexto o fecha.');
|
||||
}
|
||||
} catch (requestError) {
|
||||
setError(errorMessage(requestError));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const regenerateChecklist = async () => {
|
||||
if (!id) return;
|
||||
setBusy(true); setError(''); setSuccess('');
|
||||
try {
|
||||
applyVisit(await generateInspectionVisitChecklist(id), false);
|
||||
setSuccess('Checklist técnico recalculado con antecedentes y controles.');
|
||||
} catch (requestError) { setError(errorMessage(requestError)); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const addPreventiveAsset = async (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
if (!id || !newAssetId) return;
|
||||
setBusy(true); setError(''); setSuccess('');
|
||||
try {
|
||||
applyVisit(await replaceInspectionVisitAssets(id, [...activeAssetIds, newAssetId]), false);
|
||||
setNewAssetId('');
|
||||
setSuccess('Registro agregado como preventivo.');
|
||||
} 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 debe tener al menos 10 caracteres.');
|
||||
return;
|
||||
}
|
||||
setBusy(true); setError(''); setSuccess('');
|
||||
try {
|
||||
applyVisit(await excludeInspectionVisitAsset(id, assetId, reason), false);
|
||||
setSuccess('Registro excluido con trazabilidad.');
|
||||
} 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 historial.');
|
||||
} 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;
|
||||
const nextMembers = new Set(memberIds);
|
||||
if (leadInspectorId) nextMembers.add(leadInspectorId);
|
||||
setBusy(true); setError(''); setSuccess('');
|
||||
try {
|
||||
applyVisit(await replaceInspectionVisitTeam(id, leadInspectorId || null, [...nextMembers]), false);
|
||||
setSuccess('Equipo actualizado.');
|
||||
} catch (requestError) { setError(errorMessage(requestError)); }
|
||||
finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const lifecycleAction = async (action: 'PLAN' | 'UNPLAN' | 'CANCEL') => {
|
||||
if (!id) return;
|
||||
setBusy(true); setError(''); setSuccess('');
|
||||
try {
|
||||
let updated: InspectionVisit;
|
||||
if (action === 'PLAN') {
|
||||
updated = await planInspectionVisit(id);
|
||||
setSuccess('Inspección planificada. Ya puede iniciarse desde la APK.');
|
||||
} else if (action === 'UNPLAN') {
|
||||
updated = await unplanInspectionVisit(id);
|
||||
setSuccess('La Inspección volvió a borrador para corregir la planificación.');
|
||||
} else {
|
||||
const reason = window.prompt('Indicá el motivo de cancelación (mínimo 10 caracteres):')?.trim() ?? '';
|
||||
if (reason.length < 10) {
|
||||
if (reason) setError('El motivo debe tener al menos 10 caracteres.');
|
||||
return;
|
||||
}
|
||||
updated = await cancelInspectionVisit(id, reason);
|
||||
setSuccess('Inspección cancelada con motivo auditado.');
|
||||
}
|
||||
applyVisit(updated);
|
||||
} 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 ? 'Área, Operadora, inicio e Inspector. El identificador se genera 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>Contexto y fecha de inicio</h2><p className="section-copy">No existe título independiente ni fecha de fin planificada. El cierre real se registra al terminar el trabajo de campo.</p></div>{visit && <small className="muted">Actualizado {formatDate(visit.updatedAt)}</small>}</div>
|
||||
{visit && <div className="inspection-generated-code"><small>Identificador institucional</small><strong>{visit.code}</strong></div>}
|
||||
<div className="form-grid">
|
||||
<label className="field"><span>Área / Yacimiento</span><SearchableSelect searchPlaceholder="Buscar Área…" value={form.operationalAreaId} onChange={(event) => { context.setAreaId(event.target.value); setForm((current) => ({ ...current, operationalAreaId: event.target.value, operatorCompanyId: '' })); }} required disabled={!canManage || !planningEditable}><option value="">Seleccionar Área…</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) => { context.setCompanyId(event.target.value); setForm((current) => ({ ...current, operatorCompanyId: event.target.value })); }} required disabled={!canManage || !planningEditable || !form.operationalAreaId}><option value="">Seleccionar Operadora…</option>{operators.map((operator) => <option key={operator.id} value={operator.id}>{operator.name} · {operator.code}</option>)}</SearchableSelect></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 responsable</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} maxLength={4000} value={form.objective} onChange={(event) => setForm((current) => ({ ...current, objective: event.target.value }))} disabled={!canManage || !planningEditable} /></label><label className="field"><span>Instrucciones <em>opcional</em></span><textarea rows={2} maxLength={4000} value={form.instructions} onChange={(event) => setForm((current) => ({ ...current, instructions: event.target.value }))} disabled={!canManage || !planningEditable} /></label></>}
|
||||
{canManage && planningEditable && <div className="form-actions"><Link className="button secondary" to="/inspecciones">Volver</Link><button className="button primary" disabled={busy || (isNew && (!canAssign || !leadInspectorId))}><Icon name="check" />{isNew ? '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 TÉCNICO</span><h2>Antecedentes y controles</h2><p className="section-copy">Se construye por Área, Operadora y fecha. Las respuestas administrativas de empresa no modifican este checklist.</p></div>{canManage && planningEditable && <button type="button" className="button secondary" onClick={() => void regenerateChecklist()} disabled={busy}>Regenerar checklist</button>}</div>
|
||||
{visit.checklist.stale && <Alert>El contexto o la fecha cambió. Regenerá el checklist antes de planificar.</Alert>}
|
||||
<div className="inspection-checklist-metrics"><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.items.length === 0 ? <EmptyState title="Sin antecedentes" text="No hay Hallazgos históricos técnicos para este contexto." /> : <div className="table-scroll"><table><thead><tr><th>Tipo</th><th>Hallazgo</th><th>Inventario</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}</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>{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</span><h2>Hallazgos a controlar</h2></div><span className="count-pill">{visit.verificationFindings.length}</span></div><div className="dossier-link-list">{visit.verificationFindings.map((finding) => <Link key={finding.id} to={`/hallazgos/${finding.id}`}><div><strong>{finding.title}</strong><small>{finding.code} · {finding.assetName} · objetivo {formatDateOnly(finding.targetControlOn ?? finding.nextControlOn)}</small>{finding.resultNotes && <small>{finding.resultNotes}</small>}</div><span>{finding.outcome === 'RESOLVED' ? 'Solucionado' : finding.outcome === 'NOT_RESOLVED' ? 'No solucionado' : finding.outcome === 'REQUIRES_NEW_DATE' ? 'Reprogramar' : 'Pendiente'}</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 Inventario sin pendiente previo</h2></div></div><label className="field"><span>Buscar Inventario</span><input value={assetSearch} onChange={(event) => setAssetSearch(event.target.value)} placeholder="Código o nombre" /></label><label className="field"><span>Inventario</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 className="form-actions"><button className="button primary" disabled={busy || !newAssetId}><Icon name="plus" />Agregar preventivo</button></div></form>}
|
||||
|
||||
{visit && <article className="table-panel inspection-assets"><div className="table-summary"><strong>{visit.assets.length} incluidos</strong><span>{visit.checklist.excludedAssets} excluidos con trazabilidad</span></div>{visit.planningAssets.length === 0 ? <EmptyState title="Sin Inventarios" text="Generá el checklist o agregá un preventivo." /> : <div className="table-scroll"><table><thead><tr><th>Inventario</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>{asset.included ? 'Incluido' : 'Excluido'}</td><td>{asset.exclusionReason ?? '—'}</td><td>{canManage && planningEditable && (asset.included ? <button type="button" className="button danger-outline compact" onClick={() => void excludeAsset(asset.id)}>Excluir</button> : <button type="button" className="button secondary compact" onClick={() => void reincludeAsset(asset.id)}>Reincorporar</button>)}</td></tr>)}</tbody></table></div>}</article>}
|
||||
|
||||
{visit && canAssign && planningEditable && <form className="panel inspection-team-panel" onSubmit={saveTeam}><div className="panel-heading"><div><span className="eyebrow">EQUIPO</span><h2>Inspectores asignados</h2></div></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)} onChange={() => setMemberIds((current) => { const next = new Set(current); next.has(person.id) ? next.delete(person.id) : next.add(person.id); return next; })} disabled={person.id === leadInspectorId} /><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 || !leadInspectorId}>Guardar equipo</button></div></form>}
|
||||
|
||||
{visit && <InspectionActsPanel visit={visit} />}
|
||||
|
||||
{visit && <article className="panel survey-status-actions"><div><strong>Flujo de la Inspección</strong><p>La oficina planifica. El inicio y cierre operativo se realizan desde la APK. Una Inspección no puede cerrarse hasta que todas sus Actas estén selladas.</p></div><div className="form-actions">{canManage && visit.status === 'DRAFT' && <button type="button" className="button primary" disabled={busy} onClick={() => void lifecycleAction('PLAN')}>Confirmar planificación</button>}{canManage && visit.status === 'PLANNED' && <button type="button" className="button secondary" disabled={busy} onClick={() => void lifecycleAction('UNPLAN')}>Volver a borrador</button>}{canManage && ['DRAFT', 'PLANNED'].includes(visit.status) && <button type="button" className="button danger-outline" disabled={busy} onClick={() => void lifecycleAction('CANCEL')}>Cancelar</button>}</div></article>}
|
||||
|
||||
{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.</p></div>}
|
||||
{visit?.status === 'IN_PROGRESS' && <div className="temporal-notice"><Icon name="clipboard" /><p><strong>Inspección en curso.</strong> Actas, Hallazgos, firmas y cierre se registran desde la APK.</p></div>}
|
||||
{visit?.status === 'CANCELLED' && <Alert>Cancelada: {visit.cancellationReason}</Alert>}
|
||||
</section>;
|
||||
}
|
||||
Reference in New Issue
Block a user