F4: remove obsolete generic-status inspection editor
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
Reference in New Issue
Block a user