236 lines
9.8 KiB
TypeScript
236 lines
9.8 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';
|
|
|
|
interface Option {
|
|
id: string;
|
|
code: string;
|
|
name: string;
|
|
}
|
|
|
|
interface Inspector {
|
|
id: string;
|
|
username: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
}
|
|
|
|
interface CreatedInspection {
|
|
id: string;
|
|
code: string;
|
|
}
|
|
|
|
async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|
const response = await fetch(`/api/v3${url}`, {
|
|
credentials: 'same-origin',
|
|
...init,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
...(init?.body ? { 'Content-Type': 'application/json' } : {}),
|
|
...(init?.headers ?? {}),
|
|
},
|
|
});
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
const message = typeof payload?.message === 'string'
|
|
? payload.message
|
|
: typeof payload?.error?.message === 'string'
|
|
? payload.error.message
|
|
: `No se pudo completar la operación (${response.status}).`;
|
|
throw new Error(message);
|
|
}
|
|
return payload as T;
|
|
}
|
|
|
|
function inspectorName(person: Inspector): string {
|
|
return `${person.firstName} ${person.lastName}`.trim() || person.username;
|
|
}
|
|
|
|
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('');
|
|
const [loading, setLoading] = useState(true);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
useEffect(() => {
|
|
setLoading(true);
|
|
Promise.all([
|
|
requestJson<{ data: Option[] }>('/inspection-visits/planning-context/departments'),
|
|
requestJson<{ 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;
|
|
requestJson<{ 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;
|
|
requestJson<{ 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());
|
|
requestJson<{ 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)));
|
|
}, [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 requestJson<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 || operators.length === 0}>
|
|
<option value="">{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>
|
|
</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 && operators.length === 0 && <Alert>Esta Área no tiene una Operadora vigente. 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>;
|
|
}
|