From 5b6f90577841ecefc66a8160ccae185b62d4754a Mon Sep 17 00:00:00 2001 From: enlineawork Date: Thu, 10 Sep 2026 13:03:46 -0300 Subject: [PATCH] chore: prepare deterministic F6.1 inspection planning patch --- .../f6-1-inspection-planning-patch.yml | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 .github/workflows/f6-1-inspection-planning-patch.yml diff --git a/.github/workflows/f6-1-inspection-planning-patch.yml b/.github/workflows/f6-1-inspection-planning-patch.yml new file mode 100644 index 0000000..e053c84 --- /dev/null +++ b/.github/workflows/f6-1-inspection-planning-patch.yml @@ -0,0 +1,244 @@ +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([]);\n const [operators, setOperators] = useState([]);", + " const [departments, setDepartments] = useState([]);\n const [areas, setAreas] = useState([]);\n const [yacimientos, setYacimientos] = useState([]);\n const [operators, setOperators] = useState([]);") + 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=""" + """ + new=""" {isNew && } + + {isNew && } + """ + replace('web-v2/src/pages/InspectionVisitEditorF4Page.tsx', old, new) + + # Show preparation as a stage/action inside the inspection. + marker=""" {visit && } + + {visit &&
""" + replacement=""" {visit && } + + {visit && ['DRAFT', 'PLANNED'].includes(visit.status) &&
Preparación para campo

Revisá antecedentes, pendientes y controles de esta Inspección antes de iniciar desde la APK.

Abrir preparación
} + + {visit &&
""" + 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