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