Files
dh-inspeccion-v2/web-v2/src/pages/InspectionVisitCreateF61Page.tsx
T

225 lines
9.7 KiB
TypeScript

import { useEffect, useState } from 'react';
import type { FormEvent } from 'react';
import { Link, useNavigate } from 'react-router';
import { Alert, LoadingBlock } from '../components/Feedback';
import { Icon } from '../components/Icon';
import { SearchableSelect } from '../components/SearchableSelect';
import { apiRequest } from '../lib/api';
interface Option {
id: string;
code: string;
name: string;
}
interface Inspector {
id: string;
username: string;
firstName: string;
lastName: string;
}
interface CreatedInspection {
id: string;
code: string;
}
function inspectorName(person: Inspector): string {
return `${person.firstName} ${person.lastName}`.trim() || person.username;
}
function currentLocalDateTime(): string {
const now = new Date();
const local = new Date(now.getTime() - now.getTimezoneOffset() * 60_000);
return local.toISOString().slice(0, 16);
}
export function InspectionVisitCreateF61Page() {
const navigate = useNavigate();
const [departments, setDepartments] = useState<Option[]>([]);
const [areas, setAreas] = useState<Option[]>([]);
const [yacimientos, setYacimientos] = useState<Option[]>([]);
const [operators, setOperators] = useState<Option[]>([]);
const [inspectors, setInspectors] = useState<Inspector[]>([]);
const [departmentId, setDepartmentId] = useState('');
const [areaId, setAreaId] = useState('');
const [yacimientoId, setYacimientoId] = useState('');
const [operatorId, setOperatorId] = useState('');
const [inspectorId, setInspectorId] = useState('');
const [plannedStartAt, setPlannedStartAt] = useState(currentLocalDateTime);
const [loading, setLoading] = useState(true);
const [loadingOperators, setLoadingOperators] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
setLoading(true);
Promise.all([
apiRequest<{ data: Option[] }>('/inspection-visits/planning-context/departments'),
apiRequest<{ data: Inspector[] }>('/inspection-visits/assignees'),
]).then(([departmentResponse, inspectorResponse]) => {
setDepartments(departmentResponse.data);
setInspectors(inspectorResponse.data);
}).catch((cause) => {
setError(cause instanceof Error ? cause.message : String(cause));
}).finally(() => setLoading(false));
}, []);
useEffect(() => {
setAreaId('');
setYacimientoId('');
setOperatorId('');
setAreas([]);
setYacimientos([]);
setOperators([]);
if (!departmentId) return;
apiRequest<{ data: Option[] }>(`/inspection-visits/planning-context/departments/${departmentId}/areas`)
.then((response) => setAreas(response.data))
.catch((cause) => setError(cause instanceof Error ? cause.message : String(cause)));
}, [departmentId]);
useEffect(() => {
setYacimientoId('');
setOperatorId('');
setYacimientos([]);
setOperators([]);
if (!areaId) return;
apiRequest<{ data: Option[] }>(`/inspection-visits/planning-context/areas/${areaId}/yacimientos`)
.then((response) => setYacimientos(response.data))
.catch((cause) => setError(cause instanceof Error ? cause.message : String(cause)));
}, [areaId]);
useEffect(() => {
setOperatorId('');
setOperators([]);
if (!areaId || !plannedStartAt) return;
const parsedStart = new Date(plannedStartAt);
if (Number.isNaN(parsedStart.getTime())) return;
const at = encodeURIComponent(parsedStart.toISOString());
setLoadingOperators(true);
apiRequest<{ data: Option[] }>(`/inspection-visits/planning-context/areas/${areaId}/operators?at=${at}`)
.then((response) => {
setOperators(response.data);
if (response.data.length === 1 && response.data[0]) setOperatorId(response.data[0].id);
})
.catch((cause) => setError(cause instanceof Error ? cause.message : String(cause)))
.finally(() => setLoadingOperators(false));
}, [areaId, plannedStartAt]);
const submit = async (event: FormEvent) => {
event.preventDefault();
setError('');
if (!departmentId || !areaId || !yacimientoId || !operatorId || !plannedStartAt || !inspectorId) {
setError('Completá Departamento, Área, Yacimiento, Operadora, fecha e Inspector.');
return;
}
const parsedStart = new Date(plannedStartAt);
if (Number.isNaN(parsedStart.getTime())) {
setError('La fecha y hora de inicio no es válida.');
return;
}
setBusy(true);
try {
const created = await apiRequest<CreatedInspection>('/inspection-visits', {
method: 'POST',
body: JSON.stringify({
operationalAreaId: areaId,
scopeAssetId: yacimientoId,
operatorCompanyId: operatorId,
plannedStartAt: parsedStart.toISOString(),
leadInspectorUserId: inspectorId,
}),
});
navigate(`/inspecciones/${created.id}`, { replace: true });
} catch (cause) {
setError(cause instanceof Error ? cause.message : String(cause));
} finally {
setBusy(false);
}
};
if (loading) return <LoadingBlock label="Cargando estructura territorial…" />;
return <section className="survey-editor inspection-editor">
<div className="breadcrumb"><Link to="/inspecciones">Inspecciones</Link><span>/</span><span>Planificar inspección</span></div>
<div className="page-heading survey-editor-heading">
<div>
<span className="eyebrow">INSPECCIÓN</span>
<h1>Planificar inspección</h1>
<p>Seleccioná la ubicación respetando la jerarquía Departamento Área Yacimiento.</p>
</div>
</div>
{error && <Alert>{error}</Alert>}
<form className="panel form-panel inspection-quick-create" onSubmit={submit}>
<div className="panel-heading">
<div>
<span className="eyebrow">CREACIÓN RÁPIDA</span>
<h2>Ubicación, contexto y fecha</h2>
<p className="section-copy">La Operadora pertenece al contexto temporal del Área. El Yacimiento define el alcance físico de esta Inspección.</p>
</div>
</div>
<div className="form-grid">
<label className="field">
<span>Departamento</span>
<SearchableSelect value={departmentId} onChange={(event) => setDepartmentId(event.target.value)} searchPlaceholder="Buscar Departamento…" required>
<option value="">Seleccionar Departamento</option>
{departments.map((department) => <option key={department.id} value={department.id}>{department.name}</option>)}
</SearchableSelect>
</label>
<label className="field">
<span>Área</span>
<SearchableSelect value={areaId} onChange={(event) => setAreaId(event.target.value)} searchPlaceholder="Buscar Área…" required disabled={!departmentId}>
<option value="">Seleccionar Área</option>
{areas.map((area) => <option key={area.id} value={area.id}>{area.name}</option>)}
</SearchableSelect>
</label>
<label className="field">
<span>Yacimiento</span>
<SearchableSelect value={yacimientoId} onChange={(event) => setYacimientoId(event.target.value)} searchPlaceholder="Buscar Yacimiento…" required disabled={!areaId}>
<option value="">Seleccionar Yacimiento</option>
{yacimientos.map((yacimiento) => <option key={yacimiento.id} value={yacimiento.id}>{yacimiento.name}</option>)}
</SearchableSelect>
</label>
<label className="field">
<span>Operadora del Área</span>
<SearchableSelect value={operatorId} onChange={(event) => setOperatorId(event.target.value)} searchPlaceholder="Buscar Operadora…" required disabled={!areaId || !plannedStartAt || loadingOperators || operators.length === 0}>
<option value="">{loadingOperators ? 'Cargando Operadora…' : operators.length === 0 && areaId && plannedStartAt ? 'Sin Operadora vigente para esa fecha' : 'Seleccionar Operadora…'}</option>
{operators.map((operator) => <option key={operator.id} value={operator.id}>{operator.name}</option>)}
</SearchableSelect>
<small className="muted">Se obtiene de la relación vigente del Área para la fecha y hora de inicio.</small>
</label>
<label className="field">
<span>Fecha y hora de inicio</span>
<input type="datetime-local" value={plannedStartAt} onChange={(event) => setPlannedStartAt(event.target.value)} required />
</label>
<label className="field">
<span>Inspector responsable</span>
<SearchableSelect value={inspectorId} onChange={(event) => setInspectorId(event.target.value)} searchPlaceholder="Buscar Inspector…" required>
<option value="">Seleccionar Inspector</option>
{inspectors.map((inspector) => <option key={inspector.id} value={inspector.id}>{inspectorName(inspector)} · {inspector.username}</option>)}
</SearchableSelect>
</label>
</div>
{areaId && yacimientos.length === 0 && <Alert>Esta Área no tiene Yacimientos cargados. No se puede planificar una Inspección hasta corregir su jerarquía.</Alert>}
{areaId && plannedStartAt && !loadingOperators && operators.length === 0 && <Alert>Esta Área no tiene una Operadora vigente para la fecha seleccionada. Podés consultar el Inventario, pero no planificar una Inspección operativa hasta definir esa relación.</Alert>}
<div className="form-actions">
<Link className="button secondary" to="/inspecciones">Volver</Link>
<button className="button primary" disabled={busy || !departmentId || !areaId || !yacimientoId || !operatorId || !plannedStartAt || !inspectorId}>
<Icon name="check" />{busy ? 'Creando…' : 'Crear inspección'}
</button>
</div>
</form>
</section>;
}