fix: close F4 pre-merge review gaps

This commit is contained in:
github-actions[bot]
2026-09-08 18:06:16 +00:00
parent e1e44e6352
commit 844c1a950f
6 changed files with 48 additions and 12 deletions
@@ -49,7 +49,7 @@ export class ActAdministrationService {
COUNT(*) FILTER (WHERE f.status = 'OPEN' AND f.next_control_on IS NOT NULL) AS scheduled_control_count
FROM inspection_findings f WHERE f.act_id = ia.id
) fc ON true
WHERE ia.status IN ('CLOSED', 'RECTIFIED')
WHERE ia.status IN ('SEALED', 'CLOSED', 'RECTIFIED')
), classified AS (
SELECT *, CASE
WHEN "findingCount" > 0 AND "openFindingCount" = 0 THEN 'REGULARIZED'
@@ -110,7 +110,7 @@ export class ActAdministrationService {
const rows = await this.dataSource.query(`SELECT id, code, status FROM inspection_acts WHERE id = $1`, [actId]);
const act = rows[0];
if (!act) throw new NotFoundException({ code: 'ACT_NOT_FOUND', message: 'Acta inexistente.' });
if (!['CLOSED', 'RECTIFIED'].includes(act.status)) throw new BadRequestException({ code: 'ACT_ADMIN_REQUIRES_CLOSED', message: 'El seguimiento administrativo comienza cuando el Acta está cerrada.' });
if (!['SEALED', 'CLOSED', 'RECTIFIED'].includes(act.status)) throw new BadRequestException({ code: 'ACT_ADMIN_REQUIRES_SEALED', message: 'El seguimiento administrativo comienza cuando el Acta está sellada.' });
return act;
}
@@ -129,7 +129,7 @@ export class FieldBriefingService {
ORDER BY company_response.received_on DESC, company_response.created_at DESC, company_response.id DESC
LIMIT 1
) response ON true
WHERE act.status IN ('CLOSED', 'RECTIFIED')
WHERE act.status IN ('SEALED', 'CLOSED', 'RECTIFIED')
AND source_visit.id <> $1
AND source_visit.operational_area_id = $2::uuid
AND source_visit.operator_company_id = $3::uuid
@@ -22,6 +22,10 @@ export class F4AlignDocumentLifecycleConstraints1790000600000 implements Migrati
status IN ('WORKING','OFFICIALIZED','FROZEN','CANCELLED')
)
`);
await queryRunner.query(`
ALTER TABLE inspection_reports
ALTER COLUMN status SET DEFAULT 'WORKING'
`);
await queryRunner.query(`
ALTER TABLE inspection_acts
@@ -163,6 +167,10 @@ export class F4AlignDocumentLifecycleConstraints1790000600000 implements Migrati
SET status='FROZEN'
WHERE status IN ('WORKING','OFFICIALIZED')
`);
await queryRunner.query(`
ALTER TABLE inspection_reports
ALTER COLUMN status SET DEFAULT 'FROZEN'
`);
await queryRunner.query(`
ALTER TABLE inspection_reports
DROP CONSTRAINT IF EXISTS chk_inspection_reports_status
@@ -549,14 +549,7 @@ export class InspectionClosingService {
if (companyOutcomes.length !== 1) {
throw new ConflictException({
code: 'INSPECTION_ACT_COMPANY_OUTCOME_REQUIRED',
message: 'Debe existir exactamente una firma, disidencia o negativa del responsable de la empresa',
});
}
const companyOutcome = companyOutcomes[0];
if (companyOutcome.status === InspectionActSignatureStatus.ABSENT) {
throw new ConflictException({
code: 'INSPECTION_ACT_COMPANY_MANIFESTATION_PENDING',
message: 'La ausencia no reemplaza la firma o negativa; la manifestación de la empresa sigue pendiente',
message: 'Debe existir exactamente una firma, disidencia, negativa o ausencia documentada del responsable de la empresa',
});
}
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
const root = process.cwd();
const read = (relative: string) => fs.readFileSync(path.join(root, relative), 'utf8');
const migration = read('src/database/migrations/1790000600000-f4-align-document-lifecycle-constraints.ts');
const closing = read('src/inspection-closing/inspection-closing.service.ts');
const administration = read('src/act-administration/act-administration.service.ts');
const briefing = read('src/act-administration/field-briefing.service.ts');
test('F4 report status default is WORKING physically and rollback restores FROZEN', () => {
const up = migration.slice(migration.indexOf(' public async up('), migration.indexOf(' public async down('));
const down = migration.slice(migration.indexOf(' public async down('));
assert.match(up, /ALTER COLUMN status SET DEFAULT 'WORKING'/);
assert.match(down, /ALTER COLUMN status SET DEFAULT 'FROZEN'/);
});
test('F4 allows a documented company absence to satisfy the terminal manifestation required for sealing', () => {
const closeStart = closing.indexOf(' async close(');
const closeEnd = closing.indexOf(' async signatureContent(', closeStart);
const close = closing.slice(closeStart, closeEnd);
assert.match(close, /companyOutcomes\.length !== 1/);
assert.match(close, /ausencia documentada/);
assert.doesNotMatch(close, /INSPECTION_ACT_COMPANY_MANIFESTATION_PENDING/);
assert.doesNotMatch(close, /companyOutcome\.status === InspectionActSignatureStatus\.ABSENT/);
});
test('F4 SEALED Acts remain visible in administrative follow-up and field briefing', () => {
assert.match(administration, /WHERE ia\.status IN \('SEALED', 'CLOSED', 'RECTIFIED'\)/);
assert.match(administration, /\['SEALED', 'CLOSED', 'RECTIFIED'\]\.includes\(act\.status\)/);
assert.match(briefing, /WHERE act\.status IN \('SEALED', 'CLOSED', 'RECTIFIED'\)/);
});
+1 -1
View File
@@ -16,7 +16,7 @@ const label=(s:ActAdministrationState)=>states.find(x=>x.value===s)?.label??s;
export function ActAdministrationPage(){
const ctx=useOperationalContext(); const [state,setState]=useState<'ALL'|ActAdministrationState>('ALL'); const [items,setItems]=useState<ActAdministrationQueueItem[]>([]); const [meta,setMeta]=useState<PageMeta>({page:1,pageSize:25,total:0,totalPages:0}); const [counters,setCounters]=useState<Partial<Record<ActAdministrationState,number>>>({}); const [loading,setLoading]=useState(true); const [error,setError]=useState('');
useEffect(()=>{setLoading(true);setError('');listActAdministrationQueue({state,areaId:ctx.areaId||undefined,companyId:ctx.companyId||undefined,page:1,pageSize:50}).then(r=>{setItems(r.data);setMeta(r.meta);setCounters(r.counters)}).catch(e=>setError(errorMessage(e))).finally(()=>setLoading(false));},[state,ctx.areaId,ctx.companyId]);
return <section><div className="page-heading"><div><span className="eyebrow">SEGUIMIENTO ADMINISTRATIVO</span><h1>Seguimiento de Actas</h1><p>El plazo y la respuesta de la empresa se gestionan sobre el Acta completa. Los hallazgos quedan dentro de su expediente.</p></div></div>
return <section><div className="page-heading"><div><span className="eyebrow">SEGUIMIENTO ADMINISTRATIVO</span><h1>Seguimiento de Actas</h1><p>El plazo y la respuesta de la empresa se gestionan sobre el Acta completa. GEDO no activa el vencimiento por solo; el plazo se define cuando corresponda. Los hallazgos quedan dentro de su expediente.</p></div></div>
<div className="status-tabs">{states.map(s=><button key={s.value} className={state===s.value?'active':''} onClick={()=>setState(s.value)}>{s.label}{s.value!=='ALL'&&<small>{counters[s.value]??0}</small>}</button>)}</div>
{error&&<Alert>{error}</Alert>}{loading?<LoadingBlock label="Cargando seguimiento…"/>:items.length===0?<EmptyState title="Sin actas" text="No hay actas en este estado para el contexto seleccionado."/>:<div className="table-panel"><div className="table-summary"><strong>{meta.total} acta{meta.total===1?'':'s'}</strong></div><div className="table-scroll"><table><thead><tr><th>Acta</th><th>Área / empresa</th><th>Hallazgos</th><th>Plazo empresa</th><th>Respuesta</th><th>Estado</th><th/></tr></thead><tbody>{items.map(a=><tr key={a.actId}><td><strong>{a.actCode}</strong><small className="block-muted">{formatDate(a.occurredAt)}</small></td><td><strong>{a.areaName??'—'}</strong><small className="block-muted">{a.companyName??'—'}</small></td><td>{a.openFindingCount} abiertos / {a.findingCount}</td><td>{a.responseDueOn?formatDate(a.responseDueOn):'Sin definir'}</td><td>{a.responseReceivedOn?formatDate(a.responseReceivedOn):'Pendiente'}</td><td><span className={`status-badge ${a.adminState==='OVERDUE'||a.adminState==='COMMITMENT_OVERDUE'?'danger':'pending'}`}>{label(a.adminState)}</span></td><td><Link className="button secondary" to={`/seguimiento-actas/${a.actId}`}>Gestionar</Link></td></tr>)}</tbody></table></div></div>}</section>;
}