Files
dh-inspeccion-v2/.github/workflows/f6-1-inspection-planning-patch.yml
T
Workflow config file is invalid. Please check your config file: resolve aliases: yaml: line 53: could not find expected ':'

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