import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import ExcelJS from 'exceljs'; import { createHash, randomUUID } from 'node:crypto'; import { extname } from 'node:path'; import { FilesService } from '../files/files.service'; import { PrismaService } from '../prisma/prisma.service'; import { SmsConfigService } from '../sms-config/sms-config.service'; import type { AnalyzeImportOptions, CreateImportProfileDto, CreateReportBatchDto, EmbeddedImage, ImportCommitDto, ImportMapping, PagedQuery, ReportBatchInspection, ReportBatchTarget, ReviewImportItemsDto } from './report-materials.contracts'; import { profileData, validateProfile, loadWorkbook, assertSafeWorkbook, safeSpreadsheetText, readEmbeddedImages, suggestMappings, remapProfileColumns, signatureCoreMapping, drainageCoreMapping, normalizeHeader, normalizeFieldCode, clamp, normalizePage, normalizePageSize, dateRange, cellText, transformValue, mappedCoreValue, dynamicValues, jsonRecord, hasValue, isFileRef, resolveExportValue, applyExportTransform, styleHeader, normalizeImageExtension, imageContentType, safeFileName, normalizeBatchIdempotencyKey, jsonStringArray, jsonSafe } from './report-materials.helpers'; /** R4 report-materials domain service composed behind ReportMaterialsService. */ export class ReportPendingQueryService { constructor(private readonly prisma: PrismaService, private readonly files: FilesService, private readonly smsConfig: SmsConfigService) {} async listPending(query: { reportType?: 'signature' | 'drainage'; tenantId?: string; applicationId?: string } & PagedQuery) { const items = await this.findPendingItems(query); const page = normalizePage(query.page); const pageSize = normalizePageSize(query.pageSize); return { items: items.slice((page - 1) * pageSize, page * pageSize), total: items.length, page, pageSize, }; } async findPendingItems(query: { reportType?: 'signature' | 'drainage'; tenantId?: string; applicationId?: string } & PagedQuery) { const changedAt = dateRange(query.startAt, query.endAt); const keyword = query.keyword?.trim(); const [signatures, drainageInfos] = await Promise.all([ query.reportType === 'drainage' ? Promise.resolve([]) : this.prisma.smsSignature.findMany({ where: { pendingReport: true, auditStatus: 'approved', tenantId: query.tenantId, applicationId: query.applicationId, reportChangedAt: changedAt, OR: keyword ? [ { name: { contains: keyword } }, { tenant: { name: { contains: keyword } } }, { application: { name: { contains: keyword } } }, ] : undefined, }, include: { tenant: true, application: true }, orderBy: { reportChangedAt: 'desc' }, }), query.reportType === 'signature' ? Promise.resolve([]) : this.prisma.smsDrainageInfo.findMany({ where: { pendingReport: true, auditStatus: 'approved', tenantId: query.tenantId, applicationId: query.applicationId, reportChangedAt: changedAt, OR: keyword ? [ { siteName: { contains: keyword } }, { url: { contains: keyword } }, { signature: { name: { contains: keyword } } }, { tenant: { name: { contains: keyword } } }, { application: { name: { contains: keyword } } }, ] : undefined, }, include: { tenant: true, application: true, signature: true }, orderBy: { reportChangedAt: 'desc' }, }), ]); return [ ...signatures.map((item) => ({ id: `signature:${item.id}`, reportType: 'signature', signatureId: item.id, drainageItemId: null, materialVersion: item.materialVersion, changedAt: item.reportChangedAt, name: item.name, detail: item.purpose, tenant: item.tenant, application: item.application })), ...drainageInfos.map((item) => ({ id: `drainage:${item.id}`, reportType: 'drainage', signatureId: item.signatureId, drainageItemId: item.id, materialVersion: item.materialVersion, changedAt: item.reportChangedAt, name: item.siteName, detail: item.url, signatureName: item.signature.name, tenant: item.tenant, application: item.application })), ].sort((left, right) => new Date(right.changedAt).getTime() - new Date(left.changedAt).getTime()); } }