245 lines
17 KiB
YAML
245 lines
17 KiB
YAML
name: F6.1 inspection planning patch
|
|
|
|
on:
|
|
push:
|
|
branches: ['hotfix/f6-1-inspection-planning-hierarchy']
|
|
paths: ['.github/workflows/f6-1-inspection-planning-patch.yml']
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
jobs:
|
|
patch:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
ref: hotfix/f6-1-inspection-planning-hierarchy
|
|
fetch-depth: 0
|
|
- name: Apply deterministic source changes
|
|
run: |
|
|
python - <<'PY'
|
|
from pathlib import Path
|
|
|
|
def replace(path, old, new, count=1):
|
|
p=Path(path)
|
|
text=p.read_text()
|
|
if old not in text:
|
|
raise SystemExit(f'pattern not found in {path}: {old[:120]!r}')
|
|
updated=text.replace(old,new,count)
|
|
p.write_text(updated)
|
|
|
|
# Register explicit hierarchy service.
|
|
replace('api-v3/src/inspection-visits/inspection-visits.module.ts',
|
|
"import { FieldInventoryService } from './field-inventory.service';\nimport { InspectionVisitLifecycleService } from './inspection-visit-lifecycle.service';",
|
|
"import { FieldInventoryService } from './field-inventory.service';\nimport { InspectionPlanningHierarchyService } from './inspection-planning-hierarchy.service';\nimport { InspectionVisitLifecycleService } from './inspection-visit-lifecycle.service';")
|
|
replace('api-v3/src/inspection-visits/inspection-visits.module.ts',
|
|
" { provide: InspectionVisitsService, useClass: F4InspectionVisitsService },\n InspectionVisitLifecycleService,",
|
|
" { provide: InspectionVisitsService, useClass: F4InspectionVisitsService },\n InspectionPlanningHierarchyService,\n InspectionVisitLifecycleService,")
|
|
|
|
# Make Yacimiento the WEB inspection scope while preserving Area as frozen context.
|
|
replace('api-v3/src/inspection-visits/inspection-visits.service.ts',
|
|
"import { nextInspectionVisitCode } from './inspection-visit-code';",
|
|
"import { nextInspectionVisitCode } from './inspection-visit-code';\nimport { validateInspectionPlanningScope } from './inspection-planning-scope';")
|
|
replace('api-v3/src/inspection-visits/inspection-visits.service.ts',
|
|
" const operationalAreaId = dto.operationalAreaId;\n const operatorCompanyId = dto.operatorCompanyId;\n await this.validatePlanningContext(manager, operationalAreaId, operatorCompanyId);\n await this.requireAsset(manager, operationalAreaId);",
|
|
" const operationalAreaId = dto.operationalAreaId;\n const operatorCompanyId = dto.operatorCompanyId;\n const scopeAssetId = dto.scopeAssetId ?? operationalAreaId;\n await this.validatePlanningContext(manager, operationalAreaId, operatorCompanyId);\n await this.requireAsset(manager, scopeAssetId);\n await validateInspectionPlanningScope(manager, operationalAreaId, scopeAssetId);")
|
|
replace('api-v3/src/inspection-visits/inspection-visits.service.ts',
|
|
" scopeAssetId: operationalAreaId,\n operationalAreaId,",
|
|
" scopeAssetId,\n operationalAreaId,")
|
|
|
|
# F6 ownership is Area relation based; Inventory company snapshot is historical only.
|
|
old=""" SELECT COUNT(*)::integer AS outside
|
|
FROM assets asset
|
|
WHERE asset.id = ANY($1::uuid[])
|
|
AND (
|
|
asset.operational_area_id IS DISTINCT FROM $2::uuid
|
|
OR asset.operator_company_id IS DISTINCT FROM $3::uuid
|
|
)
|
|
`, [assetIds, operationalAreaId, operatorCompanyId])) as Array<{ outside: number }>;"""
|
|
new=""" SELECT COUNT(*)::integer AS outside
|
|
FROM assets asset
|
|
WHERE asset.id = ANY($1::uuid[])
|
|
AND asset.operational_area_id IS DISTINCT FROM $2::uuid
|
|
`, [assetIds, operationalAreaId])) as Array<{ outside: number }>;
|
|
// operatorCompanyId is validated against the Area temporal relation. It is
|
|
// intentionally not compared with assets.operator_company_id (historical snapshot).
|
|
void operatorCompanyId;"""
|
|
replace('api-v3/src/inspection-visits/inspection-visits.service.ts', old, new)
|
|
|
|
old=""" FROM inspection_findings finding
|
|
INNER JOIN assets asset ON asset.id = finding.asset_id
|
|
WHERE asset.operational_area_id = $3
|
|
AND asset.operator_company_id = $4
|
|
AND finding.status <> 'VOIDED'
|
|
ORDER BY finding.created_at, finding.id
|
|
`, [
|
|
visit.id,
|
|
generation,
|
|
visit.operationalAreaId,
|
|
visit.operatorCompanyId,
|
|
visit.plannedStartAt.toISOString().slice(0, 10),
|
|
]);"""
|
|
new=""" FROM inspection_findings finding
|
|
INNER JOIN assets asset ON asset.id = finding.asset_id
|
|
WHERE asset.operational_area_id = $3
|
|
AND finding.status <> 'VOIDED'
|
|
AND ($4::uuid IS NULL OR EXISTS (
|
|
WITH RECURSIVE ancestors AS (
|
|
SELECT current_asset.id,current_asset.parent_id
|
|
FROM assets current_asset WHERE current_asset.id=finding.asset_id
|
|
UNION ALL
|
|
SELECT parent.id,parent.parent_id
|
|
FROM assets parent INNER JOIN ancestors child ON child.parent_id=parent.id
|
|
)
|
|
SELECT 1 FROM ancestors WHERE id=$4::uuid LIMIT 1
|
|
))
|
|
ORDER BY finding.created_at, finding.id
|
|
`, [
|
|
visit.id,
|
|
generation,
|
|
visit.operationalAreaId,
|
|
visit.scopeAssetId,
|
|
visit.plannedStartAt.toISOString().slice(0, 10),
|
|
]);"""
|
|
replace('api-v3/src/inspection-visits/inspection-visits.service.ts', old, new)
|
|
|
|
# WEB API: explicit planning levels and scope.
|
|
old="""export async function listInspectionPlanningAreas() {
|
|
return (await apiRequest<{ data: InspectionPlanningContextAsset[] }>('/inspection-visits/planning-context/areas')).data;
|
|
}
|
|
|
|
export async function listInspectionPlanningOperators(areaId: string) {
|
|
return (await apiRequest<{ data: InspectionPlanningContextAsset[] }>(`/inspection-visits/planning-context/areas/${areaId}/operators`)).data;
|
|
}
|
|
|
|
export function createInspectionVisit(input: {
|
|
operationalAreaId: string;
|
|
operatorCompanyId: string;
|
|
plannedStartAt: string;
|
|
leadInspectorUserId: string;
|
|
}) {"""
|
|
new="""export async function listInspectionPlanningDepartments() {
|
|
return (await apiRequest<{ data: InspectionPlanningContextAsset[] }>('/inspection-visits/planning-context/departments')).data;
|
|
}
|
|
|
|
export async function listInspectionPlanningAreasForDepartment(departmentId: string) {
|
|
return (await apiRequest<{ data: InspectionPlanningContextAsset[] }>(`/inspection-visits/planning-context/departments/${departmentId}/areas`)).data;
|
|
}
|
|
|
|
export async function listInspectionPlanningAreas() {
|
|
return (await apiRequest<{ data: InspectionPlanningContextAsset[] }>('/inspection-visits/planning-context/areas')).data;
|
|
}
|
|
|
|
export async function listInspectionPlanningYacimientos(areaId: string) {
|
|
return (await apiRequest<{ data: InspectionPlanningContextAsset[] }>(`/inspection-visits/planning-context/areas/${areaId}/yacimientos`)).data;
|
|
}
|
|
|
|
export async function listInspectionPlanningOperators(areaId: string) {
|
|
return (await apiRequest<{ data: InspectionPlanningContextAsset[] }>(`/inspection-visits/planning-context/areas/${areaId}/operators`)).data;
|
|
}
|
|
|
|
export function createInspectionVisit(input: {
|
|
operationalAreaId: string;
|
|
scopeAssetId: string;
|
|
operatorCompanyId: string;
|
|
plannedStartAt: string;
|
|
leadInspectorUserId: string;
|
|
}) {"""
|
|
replace('web-v2/src/lib/api.ts', old, new)
|
|
|
|
# New inspection page state/imports.
|
|
replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx',
|
|
" listInspectionPlanningAreas,\n listInspectionPlanningOperators,",
|
|
" listInspectionPlanningAreasForDepartment,\n listInspectionPlanningDepartments,\n listInspectionPlanningOperators,\n listInspectionPlanningYacimientos,")
|
|
replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx',
|
|
"interface VisitForm {\n objective: string;\n operationalAreaId: string;",
|
|
"interface VisitForm {\n objective: string;\n departmentId: string;\n operationalAreaId: string;\n yacimientoId: string;")
|
|
replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx',
|
|
"const emptyForm: VisitForm = {\n objective: '',\n operationalAreaId: '',",
|
|
"const emptyForm: VisitForm = {\n objective: '',\n departmentId: '',\n operationalAreaId: '',\n yacimientoId: '',")
|
|
replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx',
|
|
" const [areas, setAreas] = useState<InspectionPlanningContextAsset[]>([]);\n const [operators, setOperators] = useState<InspectionPlanningContextAsset[]>([]);",
|
|
" const [departments, setDepartments] = useState<InspectionPlanningContextAsset[]>([]);\n const [areas, setAreas] = useState<InspectionPlanningContextAsset[]>([]);\n const [yacimientos, setYacimientos] = useState<InspectionPlanningContextAsset[]>([]);\n const [operators, setOperators] = useState<InspectionPlanningContextAsset[]>([]);")
|
|
replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx',
|
|
" objective: value.objective ?? '',\n operationalAreaId: value.operationalArea?.id ?? '',\n operatorCompanyId:",
|
|
" objective: value.objective ?? '',\n departmentId: '',\n operationalAreaId: value.operationalArea?.id ?? '',\n yacimientoId: value.scopeAsset?.typeName?.toLowerCase() === 'yacimiento' ? value.scopeAsset.id : '',\n operatorCompanyId:")
|
|
old=""" useEffect(() => {
|
|
listInspectionPlanningAreas().then(setAreas).catch(() => undefined);
|
|
if (canAssign) listInspectionAssignees().then(setAssignees).catch(() => undefined);
|
|
}, [canAssign]);"""
|
|
new=""" useEffect(() => {
|
|
listInspectionPlanningDepartments().then(setDepartments).catch(() => setDepartments([]));
|
|
if (canAssign) listInspectionAssignees().then(setAssignees).catch(() => undefined);
|
|
}, [canAssign]);
|
|
|
|
useEffect(() => {
|
|
if (!form.departmentId) {
|
|
if (isNew) setAreas([]);
|
|
return;
|
|
}
|
|
listInspectionPlanningAreasForDepartment(form.departmentId).then(setAreas).catch(() => setAreas([]));
|
|
}, [form.departmentId, isNew]);
|
|
|
|
useEffect(() => {
|
|
if (!form.operationalAreaId) {
|
|
setYacimientos([]);
|
|
return;
|
|
}
|
|
listInspectionPlanningYacimientos(form.operationalAreaId).then(setYacimientos).catch(() => setYacimientos([]));
|
|
}, [form.operationalAreaId]);"""
|
|
replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx', old, new)
|
|
|
|
# Do not auto-flatten global Area context into the three-level create form.
|
|
old=""" useEffect(() => {
|
|
if (!isNew || form.operationalAreaId || !context.areaId) return;
|
|
setForm((current) => ({
|
|
...current,
|
|
operationalAreaId: context.areaId,
|
|
operatorCompanyId: context.companyId,
|
|
}));
|
|
}, [isNew, form.operationalAreaId, context.areaId, context.companyId]);
|
|
|
|
"""
|
|
replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx', old, '')
|
|
|
|
replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx',
|
|
" if (!form.operationalAreaId || !form.operatorCompanyId || !plannedStartAt || !leadInspectorId) {\n setError('Seleccioná Área, Operadora, fecha de inicio e Inspector.');",
|
|
" if (!form.departmentId || !form.operationalAreaId || !form.yacimientoId || !form.operatorCompanyId || !plannedStartAt || !leadInspectorId) {\n setError('Seleccioná Departamento, Área, Yacimiento, Operadora, fecha de inicio e Inspector.');")
|
|
replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx',
|
|
" operationalAreaId: form.operationalAreaId,\n operatorCompanyId: form.operatorCompanyId,",
|
|
" operationalAreaId: form.operationalAreaId,\n scopeAssetId: form.yacimientoId,\n operatorCompanyId: form.operatorCompanyId,", 1)
|
|
replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx',
|
|
"'Área, Operadora, inicio e Inspector. El identificador se genera automáticamente.'",
|
|
"'Departamento, Área, Yacimiento, Operadora, inicio e Inspector. El identificador se genera automáticamente.'")
|
|
|
|
old=""" <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>"""
|
|
new=""" {isNew && <label className=\"field\"><span>Departamento</span><SearchableSelect searchPlaceholder=\"Buscar Departamento…\" value={form.departmentId} onChange={(event) => setForm((current) => ({ ...current, departmentId: event.target.value, operationalAreaId: '', yacimientoId: '', operatorCompanyId: '' }))} required disabled={!canManage || !planningEditable}><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 searchPlaceholder=\"Buscar Área…\" value={form.operationalAreaId} onChange={(event) => { context.setAreaId(event.target.value); setForm((current) => ({ ...current, operationalAreaId: event.target.value, yacimientoId: '', operatorCompanyId: '' })); }} required disabled={!canManage || !planningEditable || (isNew && !form.departmentId)}><option value=\"\">Seleccionar Área…</option>{areas.map((area) => <option key={area.id} value={area.id}>{area.name}</option>)}</SearchableSelect></label>
|
|
{isNew && <label className=\"field\"><span>Yacimiento</span><SearchableSelect searchPlaceholder=\"Buscar Yacimiento…\" value={form.yacimientoId} onChange={(event) => setForm((current) => ({ ...current, yacimientoId: event.target.value }))} required disabled={!canManage || !planningEditable || !form.operationalAreaId}><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</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}</option>)}</SearchableSelect></label>"""
|
|
replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx', old, new)
|
|
|
|
# Show preparation as a stage/action inside the inspection.
|
|
marker=""" {visit && <InspectionActsPanel visit={visit} />}
|
|
|
|
{visit && <article className=\"panel survey-status-actions\">"""
|
|
replacement=""" {visit && <InspectionActsPanel visit={visit} />}
|
|
|
|
{visit && ['DRAFT', 'PLANNED'].includes(visit.status) && <article className=\"panel survey-status-actions\"><div><strong>Preparación para campo</strong><p>Revisá antecedentes, pendientes y controles de esta Inspección antes de iniciar desde la APK.</p></div><div className=\"form-actions\"><Link className=\"button secondary\" to={`/preparacion-campo?inspectionId=${visit.id}`}><Icon name=\"clipboard\" />Abrir preparación</Link></div></article>}
|
|
|
|
{visit && <article className=\"panel survey-status-actions\">"""
|
|
replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx', marker, replacement)
|
|
|
|
PY
|
|
- name: Commit patched sources
|
|
run: |
|
|
set -Eeuo pipefail
|
|
git config user.name 'github-actions[bot]'
|
|
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
|
git add api-v3 web-v2
|
|
git diff --cached --check
|
|
git commit -m 'fix(F6.1): separate Departamento Area Yacimiento in inspections'
|
|
git push origin HEAD:hotfix/f6-1-inspection-planning-hierarchy
|