diff --git a/.github/workflows/f6-1-inspection-planning-patch.yml b/.github/workflows/f6-1-inspection-planning-patch.yml deleted file mode 100644 index e053c84..0000000 --- a/.github/workflows/f6-1-inspection-planning-patch.yml +++ /dev/null @@ -1,244 +0,0 @@ -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