import { Injectable, NotFoundException } from '@nestjs/common'; import { DataSource } from 'typeorm'; import type { ListInspectionActsQueryDto } from './dto/list-inspection-acts-query.dto'; /** * Read model intentionally kept small for the field client. * * The Android app only needs the Act identity/lifecycle, deadlines and counters to * open an Inspection. It must not fail because an office-document/report module is * unavailable or temporarily schema-drifted. The canonical office endpoints keep * their richer projection in InspectionActsService. */ @Injectable() export class MobileInspectionActsService { constructor(private readonly dataSource: DataSource) {} async listForVisit(visitId: string, query: ListInspectionActsQueryDto) { const [visit] = (await this.dataSource.query( 'SELECT id FROM inspection_visits WHERE id = $1::uuid', [visitId], )) as Array<{ id: string }>; if (!visit) { throw new NotFoundException({ code: 'INSPECTION_VISIT_NOT_FOUND', message: 'Visita de inspección no encontrada', }); } const conditions = ['act.visit_id = $1::uuid']; const parameters: unknown[] = [visitId]; const add = (value: unknown): string => { parameters.push(value); return `$${parameters.length}`; }; if (query.search?.trim()) { const search = add(`%${query.search.trim()}%`); conditions.push(`(act.code ILIKE ${search} OR act.title ILIKE ${search})`); } if (query.status) conditions.push(`act.status = ${add(query.status)}`); if (query.year) conditions.push(`act.act_year = ${add(query.year)}`); if (query.dateFrom) conditions.push(`act.occurred_at::date >= ${add(query.dateFrom)}::date`); if (query.dateTo) conditions.push(`act.occurred_at::date <= ${add(query.dateTo)}::date`); const where = `WHERE ${conditions.join(' AND ')}`; const [countRow] = (await this.dataSource.query( `SELECT COUNT(*)::integer AS total FROM inspection_acts act ${where}`, parameters, )) as Array<{ total: number }>; const total = Number(countRow?.total ?? 0); const limit = add(query.pageSize); const offset = add((query.page - 1) * query.pageSize); const data = await this.dataSource.query(` SELECT act.id, act.visit_id AS "visitId", act.code, act.status, act.occurred_at AS "occurredAt", act.title, act.summary, act.observations, act.urgency, act.deadline_days AS "deadlineDays", act.deadline_day_type AS "deadlineDayType", act.deadline_basis AS "deadlineBasis", act.deadline_base_at AS "deadlineBaseAt", act.deadline_at AS "deadlineAt", act.locked_at AS "lockedAt", act.locked_sha256 AS "lockedSha256", act.sealed_at AS "sealedAt", act.current_version AS "currentVersion", act.closed_at AS "closedAt", act.closure_sha256 AS "closureSha256", COALESCE(( SELECT COUNT(*)::integer FROM inspection_act_assets link WHERE link.act_id = act.id AND link.included = true ), 0)::integer AS "assetCount", COALESCE(( SELECT COUNT(*)::integer FROM inspection_findings finding WHERE finding.act_id = act.id AND finding.status <> 'VOIDED' ), 0)::integer AS "findingCount" FROM inspection_acts act ${where} ORDER BY act.act_year DESC, act.act_number DESC LIMIT ${limit} OFFSET ${offset} `, parameters); return { data, meta: { page: query.page, pageSize: query.pageSize, total, totalPages: total === 0 ? 0 : Math.ceil(total / query.pageSize), }, }; } }