fix(f6.9): include every field photo and preserve document revisions
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Successful in 3m8s
DH V2 CI / WEB · typecheck, build (push) Successful in 19s
DH V2 CI / API · typecheck, tests, build (push) Successful in 32s
Production dependency audit / API · production dependencies (push) Successful in 8s
Production dependency audit / WEB · production dependencies (push) Successful in 9s
DH V2 CI / Docker / scripts contract (push) Successful in 59s
Android CI / RC / Android · lint, tests, debug APK, release compile (push) Successful in 3m8s
DH V2 CI / WEB · typecheck, build (push) Successful in 19s
DH V2 CI / API · typecheck, tests, build (push) Successful in 32s
Production dependency audit / API · production dependencies (push) Successful in 8s
Production dependency audit / WEB · production dependencies (push) Successful in 9s
DH V2 CI / Docker / scripts contract (push) Successful in 59s
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class F69ConsolidatedDocumentRevisions1790135400000 implements MigrationInterface {
|
||||
name = 'F69ConsolidatedDocumentRevisions1790135400000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE inspection_act_consolidated_pdf_revisions (
|
||||
act_id uuid NOT NULL REFERENCES inspection_acts(id) ON DELETE CASCADE,
|
||||
template_version smallint NOT NULL CHECK (template_version >= 1),
|
||||
stored_name varchar(255) NOT NULL,
|
||||
original_name varchar(255) NOT NULL,
|
||||
size_bytes integer NOT NULL CHECK (size_bytes > 0),
|
||||
sha256 char(64) NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),
|
||||
generated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (act_id,template_version)
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
INSERT INTO inspection_act_consolidated_pdf_revisions(
|
||||
act_id,template_version,stored_name,original_name,size_bytes,sha256,generated_at)
|
||||
SELECT act_id,1,stored_name,original_name,size_bytes,sha256,generated_at
|
||||
FROM inspection_act_consolidated_pdf_artifacts
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE inspection_report_consolidated_word_revisions (
|
||||
report_id uuid NOT NULL REFERENCES inspection_reports(id) ON DELETE CASCADE,
|
||||
template_version smallint NOT NULL CHECK (template_version >= 1),
|
||||
stored_name varchar(255) NOT NULL,
|
||||
original_name varchar(255) NOT NULL,
|
||||
size_bytes integer NOT NULL CHECK (size_bytes > 0),
|
||||
sha256 char(64) NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),
|
||||
generated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (report_id,template_version)
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
INSERT INTO inspection_report_consolidated_word_revisions(
|
||||
report_id,template_version,stored_name,original_name,size_bytes,sha256,generated_at)
|
||||
SELECT report_id,1,stored_name,original_name,size_bytes,sha256,generated_at
|
||||
FROM inspection_report_consolidated_word_artifacts
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP TABLE inspection_report_consolidated_word_revisions');
|
||||
await queryRunner.query('DROP TABLE inspection_act_consolidated_pdf_revisions');
|
||||
}
|
||||
}
|
||||
@@ -64,10 +64,10 @@ export async function buildInspectionActPdf(snapshot: Record<string, unknown>, i
|
||||
const body = (value: unknown) => { need(22); doc.font('body').fillColor('#202939').fontSize(10.5).text(text(value, '-'), { lineGap: 3 }); doc.moveDown(0.4); };
|
||||
const label = (name: string, value: unknown) => { if (!text(value)) return; need(22); doc.font('body-bold').fillColor('#202939').fontSize(10).text(`${name}: `, { continued: true }); doc.font('body').text(text(value)); doc.moveDown(0.35); };
|
||||
const image = (entry: ActPdfImage, caption: string) => {
|
||||
need(290);
|
||||
need(235);
|
||||
const y = doc.y;
|
||||
doc.image(entry.buffer, 58, y, { fit: [470, 245] });
|
||||
doc.y = y + 250;
|
||||
doc.image(entry.buffer, 58, y, { fit: [470, 190] });
|
||||
doc.y = y + 195;
|
||||
doc.font('body').fontSize(8).fillColor('#47536A').text(`${caption} · SHA-256 ${entry.sha256}`, 58, doc.y, { width: 475 });
|
||||
doc.moveDown(0.5);
|
||||
};
|
||||
@@ -108,7 +108,11 @@ export async function buildInspectionActPdf(snapshot: Record<string, unknown>, i
|
||||
}
|
||||
doc.moveDown(0.4);
|
||||
}
|
||||
if (!findings.length) for (const photo of images.filter((item) => item.assetId)) image(photo, `Fotografía de inventario ${text(photo.title)}`);
|
||||
const otherAssetPhotos = images.filter((item) => item.assetId && !shownAssetPhotos.has(item.id));
|
||||
if (otherAssetPhotos.length) {
|
||||
heading('Otras instalaciones inspeccionadas');
|
||||
for (const photo of otherAssetPhotos) image(photo, `Fotografía de inventario ${text(photo.title)}${photo.capturedAt ? ` · ${date(photo.capturedAt)}` : ''}`);
|
||||
}
|
||||
heading('Intervinientes y firmas');
|
||||
for (const signature of signatures) {
|
||||
const name = text(signature.signerName);
|
||||
|
||||
@@ -95,7 +95,8 @@ export class InspectionActPdfService {
|
||||
const [existing] = await this.dataSource.query(`
|
||||
SELECT stored_name AS "storedName",original_name AS "originalName",
|
||||
size_bytes AS "sizeBytes",sha256
|
||||
FROM inspection_act_consolidated_pdf_artifacts WHERE act_id=$1
|
||||
FROM inspection_act_consolidated_pdf_revisions
|
||||
WHERE act_id=$1 AND template_version=2
|
||||
`, [actId]) as Array<{ storedName: string; originalName: string; sizeBytes: number; sha256: string }>;
|
||||
if (existing) {
|
||||
const buffer = await this.verifiedImage(this.root, existing);
|
||||
@@ -124,13 +125,13 @@ export class InspectionActPdfService {
|
||||
if (!previous.equals(built.buffer)) throw this.storageError();
|
||||
});
|
||||
await this.dataSource.query(`
|
||||
INSERT INTO inspection_act_consolidated_pdf_artifacts(act_id,stored_name,original_name,size_bytes,sha256)
|
||||
VALUES($1,$2,$3,$4,$5) ON CONFLICT (act_id) DO NOTHING
|
||||
INSERT INTO inspection_act_consolidated_pdf_revisions(act_id,template_version,stored_name,original_name,size_bytes,sha256)
|
||||
VALUES($1,2,$2,$3,$4,$5) ON CONFLICT (act_id,template_version) DO NOTHING
|
||||
`, [actId, storedName, originalName, built.buffer.length, built.sha256]);
|
||||
const [saved] = await this.dataSource.query(`
|
||||
SELECT stored_name AS "storedName",original_name AS "originalName",
|
||||
size_bytes AS "sizeBytes",sha256
|
||||
FROM inspection_act_consolidated_pdf_artifacts WHERE act_id=$1
|
||||
FROM inspection_act_consolidated_pdf_revisions WHERE act_id=$1 AND template_version=2
|
||||
`, [actId]) as Array<{ storedName: string; originalName: string; sizeBytes: number; sha256: string }>;
|
||||
if (!saved) throw this.storageError();
|
||||
return {
|
||||
@@ -139,6 +140,18 @@ export class InspectionActPdfService {
|
||||
};
|
||||
}
|
||||
|
||||
async consolidatedRevisionContent(actId: string, version: number): Promise<{ buffer: Buffer; originalName: string; mimeType: string }> {
|
||||
if (![1, 2].includes(version)) throw new NotFoundException('Versión documental inexistente');
|
||||
const [row] = await this.dataSource.query(`
|
||||
SELECT stored_name AS "storedName",original_name AS "originalName",
|
||||
size_bytes AS "sizeBytes",sha256
|
||||
FROM inspection_act_consolidated_pdf_revisions
|
||||
WHERE act_id=$1 AND template_version=$2
|
||||
`, [actId, version]) as Array<{ storedName: string; originalName: string; sizeBytes: number; sha256: string }>;
|
||||
if (!row) throw new NotFoundException('Versión documental inexistente');
|
||||
return { buffer: await this.verifiedImage(this.root, row), originalName: row.originalName, mimeType: 'application/pdf' };
|
||||
}
|
||||
|
||||
private async actContext(actId: string): Promise<{ companyName: string | null; areaName: string | null; scopeName: string | null }> {
|
||||
const [row] = await this.dataSource.query(`
|
||||
SELECT company.name AS "companyName",area.name AS "areaName",scope.name AS "scopeName"
|
||||
|
||||
@@ -122,7 +122,7 @@ function imageDimensions(buffer: Buffer): { width: number; height: number } {
|
||||
return { width: 800, height: 500 };
|
||||
}
|
||||
|
||||
function drawing(relationship: number, image: Buffer, name: string, maxWidth = 4572000, maxHeight = 2743200, right = false): string {
|
||||
function drawing(relationship: number, image: Buffer, name: string, maxWidth = 4572000, maxHeight = 2057400, right = false): string {
|
||||
const dimensions = imageDimensions(image);
|
||||
const scale = Math.min(maxWidth / dimensions.width, maxHeight / dimensions.height);
|
||||
const cx = Math.round(dimensions.width * scale);
|
||||
@@ -136,6 +136,7 @@ function documentXml(input: ReportWordInput, logo: Buffer): string {
|
||||
const photos = input.photos ?? [];
|
||||
const authors = snapshot.signatures.filter((item) => text(item.signerType, '') === 'INSPECTOR').map((item) => text(item.signerName)).join(', ');
|
||||
const legalBasis = [...new Set(snapshot.findings.map((item) => text(item.legalBasis, '')).filter(Boolean))];
|
||||
const quantity = `${snapshot.findings.length} ${snapshot.findings.length === 1 ? 'hallazgo' : 'hallazgos'}`;
|
||||
const entries: string[] = [
|
||||
paragraph('MINISTERIO DE ENERGÍA Y AMBIENTE'),
|
||||
paragraph('DIRECCIÓN DE HIDROCARBUROS'),
|
||||
@@ -155,10 +156,11 @@ function documentXml(input: ReportWordInput, logo: Buffer): string {
|
||||
paragraph('MARCO LEGAL', 'Heading1'),
|
||||
...(legalBasis.length ? legalBasis.map((basis) => paragraph(basis)) : [paragraph('No se consignó normativa específica en los hallazgos del Acta fuente.')]),
|
||||
paragraph('DESCRIPCIÓN Y ANÁLISIS TÉCNICO', 'Heading1'),
|
||||
paragraph(input.reportDescription?.trim() || `Según el Acta ${text(snapshot.act.code)}, se documentaron ${snapshot.findings.length} hallazgo(s) durante la inspección. Se detallan las observaciones y evidencias consignadas a continuación.`),
|
||||
paragraph(input.reportDescription?.trim() || `Según el Acta ${text(snapshot.act.code)}, se documentaron ${quantity} durante la inspección. Se detallan las observaciones y evidencias consignadas a continuación.`),
|
||||
paragraph('FOTOS Y HALLAZGOS', 'Heading1'),
|
||||
];
|
||||
if (!snapshot.findings.length) entries.push(paragraph('El Acta fuente no registra hallazgos.'));
|
||||
const shownPhotos = new Set<number>();
|
||||
for (const finding of snapshot.findings) {
|
||||
entries.push(paragraph(`${text(finding.code)} ${text(finding.title)}`, 'Heading2'));
|
||||
entries.push(labelValue('Instalación', text(snapshot.inventories.find((item) => text(item.id) === text(finding.assetId))?.name)));
|
||||
@@ -168,13 +170,24 @@ function documentXml(input: ReportWordInput, logo: Buffer): string {
|
||||
for (let index = 0; index < photos.length; index++) {
|
||||
const photo = photos[index]!;
|
||||
if (photo.findingId !== text(finding.id) && photo.assetId !== text(finding.assetId)) continue;
|
||||
shownPhotos.add(index);
|
||||
entries.push(drawing(index + 3, photo.buffer, photo.title || text(finding.title)));
|
||||
entries.push(paragraph(`Fotografía vinculada · SHA-256 ${photo.sha256}`));
|
||||
}
|
||||
}
|
||||
const otherPhotos = photos.map((photo, index) => ({ photo, index }))
|
||||
.filter(({ photo, index }) => photo.assetId && !shownPhotos.has(index));
|
||||
if (otherPhotos.length) {
|
||||
entries.push(paragraph('OTRAS INSTALACIONES INSPECCIONADAS', 'Heading1'));
|
||||
for (const { photo, index } of otherPhotos) {
|
||||
entries.push(labelValue('Instalación', text(photo.title)));
|
||||
entries.push(drawing(index + 3, photo.buffer, photo.title || 'Fotografía de instalación'));
|
||||
entries.push(paragraph(`Fotografía de inventario · SHA-256 ${photo.sha256}`));
|
||||
}
|
||||
}
|
||||
entries.push(
|
||||
paragraph('CONCLUSIONES', 'Heading1'),
|
||||
paragraph(input.executiveSummary?.trim() || `La inspección registró ${snapshot.findings.length} hallazgo(s). Su seguimiento y la respuesta de la empresa se documentan en el Informe.`),
|
||||
paragraph(input.executiveSummary?.trim() || `La inspección registró ${quantity}. Su seguimiento y la respuesta de la empresa se documentan en el Informe.`),
|
||||
paragraph('ACTA FUENTE E INTEGRIDAD', 'Heading1'),
|
||||
labelValue('Acta sellada', text(snapshot.source.actCode ?? snapshot.act.code)),
|
||||
labelValue('SHA-256 del cierre del Acta', text(snapshot.source.actClosureSha256 ?? snapshot.sealedAct.finalSha256)),
|
||||
|
||||
@@ -129,7 +129,8 @@ export class InspectionReportWordService {
|
||||
const [existing] = await this.dataSource.query(`
|
||||
SELECT stored_name AS "storedName",original_name AS "originalName",
|
||||
size_bytes AS "sizeBytes",sha256
|
||||
FROM inspection_report_consolidated_word_artifacts WHERE report_id=$1
|
||||
FROM inspection_report_consolidated_word_revisions
|
||||
WHERE report_id=$1 AND template_version=2
|
||||
`, [reportId]) as Array<{ storedName: string; originalName: string; sizeBytes: number; sha256: string }>;
|
||||
if (existing) return this.verifiedConsolidated(existing);
|
||||
const row = await this.load(reportId);
|
||||
@@ -151,18 +152,30 @@ export class InspectionReportWordService {
|
||||
if (!(await readFile(previous.filePath)).equals(built.buffer)) throw this.storageError();
|
||||
});
|
||||
await this.dataSource.query(`
|
||||
INSERT INTO inspection_report_consolidated_word_artifacts(report_id,stored_name,original_name,size_bytes,sha256)
|
||||
VALUES($1,$2,$3,$4,$5) ON CONFLICT (report_id) DO NOTHING
|
||||
INSERT INTO inspection_report_consolidated_word_revisions(report_id,template_version,stored_name,original_name,size_bytes,sha256)
|
||||
VALUES($1,2,$2,$3,$4,$5) ON CONFLICT (report_id,template_version) DO NOTHING
|
||||
`, [reportId, storedName, originalName, built.buffer.length, built.sha256]);
|
||||
const [saved] = await this.dataSource.query(`
|
||||
SELECT stored_name AS "storedName",original_name AS "originalName",
|
||||
size_bytes AS "sizeBytes",sha256
|
||||
FROM inspection_report_consolidated_word_artifacts WHERE report_id=$1
|
||||
FROM inspection_report_consolidated_word_revisions WHERE report_id=$1 AND template_version=2
|
||||
`, [reportId]) as Array<{ storedName: string; originalName: string; sizeBytes: number; sha256: string }>;
|
||||
if (!saved) throw this.storageError();
|
||||
return this.verifiedConsolidated(saved);
|
||||
}
|
||||
|
||||
async consolidatedRevisionContent(reportId: string, version: number): Promise<{ filePath: string; originalName: string; mimeType: string }> {
|
||||
if (![1, 2].includes(version)) throw new NotFoundException('Versión documental inexistente');
|
||||
const [row] = await this.dataSource.query(`
|
||||
SELECT stored_name AS "storedName",original_name AS "originalName",
|
||||
size_bytes AS "sizeBytes",sha256
|
||||
FROM inspection_report_consolidated_word_revisions
|
||||
WHERE report_id=$1 AND template_version=$2
|
||||
`, [reportId, version]) as Array<{ storedName: string; originalName: string; sizeBytes: number; sha256: string }>;
|
||||
if (!row) throw new NotFoundException('Versión documental inexistente');
|
||||
return this.verifiedConsolidated(row);
|
||||
}
|
||||
|
||||
private async verifiedConsolidated(row: { storedName: string; originalName: string; sizeBytes: number; sha256: string }): Promise<{ filePath: string; originalName: string; mimeType: string }> {
|
||||
if (!/^[A-Za-z0-9_.-]+$/.test(row.storedName)) throw this.storageError();
|
||||
const filePath = resolve(this.root, row.storedName);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Get,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
ParseIntPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
@@ -83,6 +84,20 @@ export class InspectionReportsController {
|
||||
return response.sendFile(content.filePath);
|
||||
}
|
||||
|
||||
@Get(':id/consolidated-word/revisions/:version')
|
||||
@RequirePermissions('inspection_reports.read')
|
||||
async consolidatedWordRevision(
|
||||
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
|
||||
@Param('version', ParseIntPipe) version: number,
|
||||
@Res() response: Response,
|
||||
) {
|
||||
const content = await this.word.consolidatedRevisionContent(id, version);
|
||||
response.setHeader('Content-Type', content.mimeType);
|
||||
response.setHeader('Content-Disposition', `attachment; filename="${content.originalName.replaceAll('"', '')}"`);
|
||||
response.setHeader('Cache-Control', 'private, no-store');
|
||||
return response.sendFile(content.filePath);
|
||||
}
|
||||
|
||||
@Get(':id/gedo-pdf')
|
||||
@RequirePermissions('inspection_reports.read')
|
||||
async gedoPdfContent(
|
||||
@@ -206,6 +221,22 @@ export class InspectionActPdfController {
|
||||
export class InspectionActConsolidatedPdfController {
|
||||
constructor(private readonly pdf: InspectionActPdfService) {}
|
||||
|
||||
@Get('revisions/:version')
|
||||
@RequirePermissions('inspection_acts.read')
|
||||
async revision(
|
||||
@Param('actId', new ParseUUIDPipe({ version: '4' })) actId: string,
|
||||
@Param('version', ParseIntPipe) version: number,
|
||||
@Res() response: Response,
|
||||
) {
|
||||
const content = await this.pdf.consolidatedRevisionContent(actId, version);
|
||||
response.setHeader('Content-Type', content.mimeType);
|
||||
response.setHeader('Content-Length', String(content.buffer.length));
|
||||
response.setHeader('Content-Disposition', `inline; filename="${content.originalName.replaceAll('"', '')}"`);
|
||||
response.setHeader('Cache-Control', 'private, no-store');
|
||||
return response.send(content.buffer);
|
||||
}
|
||||
|
||||
|
||||
@Get()
|
||||
@RequirePermissions('inspection_acts.read')
|
||||
async content(
|
||||
|
||||
@@ -50,10 +50,10 @@ test('F6.9 technical Informe embeds evidence and its institutional source hash',
|
||||
test('F6.9 keeps prior sealed PDF and prior company responses available for audit', () => {
|
||||
const pdf = readFileSync(resolve(process.cwd(), 'src/inspection-reports/inspection-act-pdf.service.ts'), 'utf8');
|
||||
const controller = readFileSync(resolve(process.cwd(), 'src/act-administration/act-administration.controller.ts'), 'utf8');
|
||||
assert.match(pdf, /inspection_act_consolidated_pdf_artifacts/);
|
||||
assert.match(pdf, /inspection_act_consolidated_pdf_revisions/);
|
||||
const word = readFileSync(resolve(process.cwd(), 'src/inspection-reports/inspection-report-word.service.ts'), 'utf8');
|
||||
assert.match(word, /inspection_report_consolidated_word_artifacts/);
|
||||
assert.match(pdf, /ON CONFLICT \(act_id\) DO NOTHING/);
|
||||
assert.match(word, /inspection_report_consolidated_word_revisions/);
|
||||
assert.match(pdf, /ON CONFLICT \(act_id,template_version\) DO NOTHING/);
|
||||
assert.match(controller, /responseContent\(responseId\)/);
|
||||
assert.doesNotMatch(controller, /@Post\('responses'\)/);
|
||||
});
|
||||
@@ -84,3 +84,25 @@ test('F6.9 built CommonJS runtime renders signed PDF and WebP evidence', async (
|
||||
const pdf = await runtimePdf.buildInspectionActPdf(sealed, [{ ...evidence, buffer: rendered, sha256: digest(webp) }]);
|
||||
assert.ok(pdf.buffer.subarray(0, 8).equals(Buffer.from('%PDF-1.4')));
|
||||
});
|
||||
|
||||
test('F6.9 revision migration archives both first document versions before serving the corrected template', () => {
|
||||
const migration = readFileSync(resolve(process.cwd(),
|
||||
'src/database/migrations/1790135400000-f6-9-consolidated-document-revisions.ts'), 'utf8');
|
||||
assert.match(migration, /SELECT act_id,1,stored_name,original_name,size_bytes,sha256,generated_at/);
|
||||
assert.match(migration, /SELECT report_id,1,stored_name,original_name,size_bytes,sha256,generated_at/);
|
||||
assert.match(migration, /PRIMARY KEY \(act_id,template_version\)/);
|
||||
assert.match(migration, /PRIMARY KEY \(report_id,template_version\)/);
|
||||
});
|
||||
|
||||
test('F6.9 includes field photos from installations without their own findings', async () => {
|
||||
const other = await sharp(photo).resize(75).png().toBuffer();
|
||||
const unlinked = { id: 'photo-other-asset', assetId: 'asset-2', title: 'Otra instalación', sha256: digest(other), buffer: other };
|
||||
const withUnlinked = await buildInspectionActPdf(sealed, [evidence, unlinked]);
|
||||
const linkedOnly = await buildInspectionActPdf(sealed, [evidence]);
|
||||
assert.ok(withUnlinked.buffer.length > linkedOnly.buffer.length + 500);
|
||||
const word = buildInspectionReportWord({ code: 'INF-OTHER', title: 'Informe', generatedAt: new Date(),
|
||||
frozenSha256: 'c'.repeat(64), frozenSnapshot: { sealedAct: sealed }, photos: [evidence, unlinked] });
|
||||
assert.ok(word.buffer.includes(Buffer.from('OTRAS INSTALACIONES INSPECCIONADAS')));
|
||||
assert.ok(word.buffer.includes(Buffer.from('word/media/photo-2.png')));
|
||||
assert.ok(word.buffer.includes(other));
|
||||
});
|
||||
|
||||
@@ -66,11 +66,14 @@ export function InspectionActMediaPanel({ actId }: { actId: string }) {
|
||||
.finally(() => active && setLoading(false));
|
||||
return () => { active = false; };
|
||||
}, [actId]);
|
||||
const findingAssetIds = new Set(items.map((item) => item.finding.asset.id));
|
||||
const otherPhotos = assetPhotos.filter((photo) => !findingAssetIds.has(photo.assetId));
|
||||
return <section className="panel act-media-panel">
|
||||
<div className="panel-heading"><div><h2>Hallazgos del Acta</h2><p className="section-copy">Cada hallazgo reúne su descripción y las fotos tomadas en campo.</p></div><span className="count-pill">{items.length}</span></div>
|
||||
{error && <Alert>{error}</Alert>}
|
||||
{loading ? <LoadingBlock label="Cargando hallazgos y fotografías…" /> : items.length ?
|
||||
<div className="act-finding-list">{items.map((item) => <Finding key={item.finding.id} item={item} assetPhotos={assetPhotos.filter((photo) => photo.assetId === item.finding.asset.id)} />)}</div> :
|
||||
<EmptyState title="Sin hallazgos" text="Esta Acta no contiene hallazgos sincronizados." />}
|
||||
{!loading && otherPhotos.length > 0 && <div className="act-other-asset-photos"><h3>Fotos de otras instalaciones</h3><div className="act-finding-photos">{otherPhotos.map((photo) => <Photo key={photo.id} id={photo.id} title={photo.title || 'Instalación inspeccionada'} caption={`${photo.title || 'Instalación'} · ${formatDate(photo.fieldCapturedAt || photo.capturedAt || photo.createdAt)}`} load={getAssetMediaBlob} />)}</div></div>}
|
||||
</section>;
|
||||
}
|
||||
|
||||
@@ -1433,3 +1433,6 @@ code { color: #5e6677; font-family: ui-monospace, monospace; font-size: 9px; }
|
||||
.act-closed-date { font-weight: 700; }
|
||||
.act-integrity-details { border-top: 1px solid var(--border); padding-top: 10px; }
|
||||
.act-integrity-details code { overflow-wrap: anywhere; }
|
||||
|
||||
.act-other-asset-photos { border-top: 1px solid #e3eaf5; margin-top: 24px; padding-top: 20px; }
|
||||
.act-other-asset-photos h3 { margin: 0 0 14px; }
|
||||
|
||||
Reference in New Issue
Block a user