chore: prepare deterministic F6.1 inspection planning patch
This commit is contained in:
@@ -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<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
|
||||
Reference in New Issue
Block a user