fix(F6.1): add explicit hierarchical inspection creation page
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
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;
|
||||
Promise.all([
|
||||
requestJson<{ data: Option[] }>(`/inspection-visits/planning-context/areas/${areaId}/yacimientos`),
|
||||
requestJson<{ data: Option[] }>(`/inspection-visits/planning-context/areas/${areaId}/operators`),
|
||||
]).then(([yacimientoResponse, operatorResponse]) => {
|
||||
setYacimientos(yacimientoResponse.data);
|
||||
setOperators(operatorResponse.data);
|
||||
if (operatorResponse.data.length === 1 && operatorResponse.data[0]) {
|
||||
setOperatorId(operatorResponse.data[0].id);
|
||||
}
|
||||
}).catch((cause) => setError(cause instanceof Error ? cause.message : String(cause)));
|
||||
}, [areaId]);
|
||||
|
||||
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 || operators.length === 0}>
|
||||
<option value="">{operators.length === 0 && areaId ? 'Sin Operadora vigente' : '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 && 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user