Untitled

 avatar
unknown
plain_text
9 months ago
13 kB
12
Indexable
  async listWeighedProductions(): Promise<GetWeighedProductionsResponseDto> {
    this.logger.log('Starting listWeighedProductions')

    // statuses
    const weighedStatusId = await this.list.getListUUID(
      'batch_process_productions',
      'statuses',
      'weighed',
    )
    const currentBatchStatusId = await this.list.getListUUID('batches', 'statuses', 'current')
    const checkedBatchStatusId = await this.list.getListUUID('batches', 'statuses', 'checked')
    const forApprovalBatchStatusId = await this.list.getListUUID(
      'batches',
      'statuses',
      'for_approval',
    )

    // Use pagination and streaming approach
    const BATCH_SIZE = 50 // Reduced from 100 to be more conservative
    const items: WeighedProductionDto[] = []
    let skip = 0
    let hasMore = true
    let processedCount = 0

    while (hasMore) {
      this.logger.debug(`Processing batch ${skip / BATCH_SIZE + 1}, skip: ${skip}`)

      const productions = await this.prisma.batchProcessProduction.findMany({
        skip,
        take: BATCH_SIZE,
        where: {
          listId_status: weighedStatusId,
          batchProcess: {
            batch: {
              listId_statuses: {
                in: [currentBatchStatusId, forApprovalBatchStatusId, checkedBatchStatusId],
              },
            },
          },
        },
        select: {
          id: true,
          batchProcessId: true,
          dtproduction: true,
          material: {
            select: {
              id: true,
              description: true,
              quantityUnit: { select: { value: { select: { name: true } } } },
              weightUnit: { select: { value: { select: { name: true } } } },
            },
          },
          location: { select: { id: true, name: true } },
          quantity: true,
          weight: true,
          batchProcess: {
            select: {
              id: true,
              subinstallation: { select: { id: true, name: true } },
              processId: true,
              batchId: true,
              batch: {
                select: {
                  batchNumber: true,
                  processtypeId: true,
                  installationId: true,
                  processtype: { select: { id: true } },
                },
              },
            },
          },
        },
        orderBy: { dtproduction: 'asc' },
      })

      if (productions.length === 0) {
        hasMore = false
        break
      }

      // Batch load all batch template materials to avoid N+1 queries
      const batchTemplateMaterials = await this._getBatchTemplateMaterialsBatch(
        productions.map((p) => ({
          batchProcessId: p.batchProcess.id,
          materialId: p.material.id,
          subinstallationId: p.batchProcess.subinstallation.id,
          installationId: p.batchProcess.batch.installationId,
          processtypeId: p.batchProcess.batch.processtypeId,
        })),
      )

      // Get production IDs that have weighed materials
      const weighedProductionIds = productions
        .filter((p) => {
          const btmKey = `${p.batchProcess.id}-${p.material.id}`
          const batchTemplateMaterial = batchTemplateMaterials.get(btmKey)
          return batchTemplateMaterial?.weight?.value?.name === 'weighed'
        })
        .map((p) => p.id)

      // Batch load analysis headers for weighed productions only
      const analysisHeadersMap = await this._loadAnalysisHeadersBatch(weighedProductionIds)

      // Process this batch
      for (const p of productions) {
        const btmKey = `${p.batchProcess.id}-${p.material.id}`
        const batchTemplateMaterial = batchTemplateMaterials.get(btmKey)

        // Filter: only include productions whose batch template material weight type is 'weighed'
        if (batchTemplateMaterial?.weight?.value?.name !== 'weighed') {
          continue // skip non-weighed materials
        }

        const analysisHeaders = analysisHeadersMap.get(p.id)

        items.push({
          productionId: p.id,
          batchProcessId: p.batchProcessId,
          batchId: p.batchProcess.batchId,
          batchNumber: (p.batchProcess.batch as any)?.batchNumber ?? null,
          subinstallationName: p.batchProcess.subinstallation.name,
          dtproduction: p.dtproduction,
          materialId: p.material.id,
          materialDescription: p.material.description,
          locationId: p.location.id,
          locationName: p.location.name,
          quantity: p.quantity?.toNumber() ?? null,
          weight: p.weight.toNumber(),
          weightUnit: p.material.weightUnit?.value?.name ?? '',
          quantityUnit: p.material.quantityUnit?.value?.name ?? '',
          analysisHeaders,
        })

        processedCount++
      }

      skip += BATCH_SIZE
      hasMore = productions.length === BATCH_SIZE

      this.logger.debug(`Processed ${processedCount} weighed productions so far`)
    }

    this.logger.log(`Completed listWeighedProductions with ${items.length} items`)
    return { items }
  }

  /**
   * Batch load batch template materials to avoid N+1 queries
   */
  private async _getBatchTemplateMaterialsBatch(
    requests: Array<{
      batchProcessId: string
      materialId: string
      subinstallationId: string
      installationId: string
      processtypeId: string
    }>,
  ): Promise<Map<string, { weight?: { value: { name: string } } | null }>> {
    if (requests.length === 0) return new Map()

    const productionTypeId = await this.list.getListUUID(
      'batchtemplate_materials',
      'types',
      'production',
    )

    // Create unique pairs without string manipulation
    const uniquePairs = Array.from(
      new Map(
        requests.map((r) => [
          `${r.installationId}|${r.subinstallationId}`, // Use | separator to avoid UUID conflicts
          { installationId: r.installationId, subinstallationId: r.subinstallationId },
        ]),
      ).values(),
    )

    const installationSubinstallations = await this.prisma.installationSubinstallation.findMany({
      where: {
        OR: uniquePairs,
      },
      select: {
        id: true,
        subinstallationId: true,
        installationId: true,
      },
    })

    const isMap = new Map(
      installationSubinstallations.map((is) => [
        `${is.installationId}|${is.subinstallationId}`,
        is.id,
      ]),
    )

    // Get unique processtype and material combinations
    const uniqueProcesstypes = Array.from(new Set(requests.map((r) => r.processtypeId)))
    const uniqueMaterials = Array.from(new Set(requests.map((r) => r.materialId)))

    // Get all batch template materials in one query
    const isps = await this.prisma.installationSubinstallationProcesstype.findMany({
      where: {
        processtypeId: { in: uniqueProcesstypes },
        installationSubinstallationId: { in: Array.from(isMap.values()) },
      },
      include: {
        batchtemplate: {
          include: {
            batchTemplateMaterials: {
              where: {
                listId_types: productionTypeId,
                materialId: { in: uniqueMaterials },
              },
              include: { weight: { include: { value: true } } },
            },
          },
        },
      },
    })

    // Build result map
    const result = new Map<string, { weight?: { value: { name: string } } | null }>()

    for (const request of requests) {
      const key = `${request.batchProcessId}-${request.materialId}`
      const isId = isMap.get(`${request.installationId}|${request.subinstallationId}`)

      const isp = isps.find(
        (i) =>
          i.processtypeId === request.processtypeId && i.installationSubinstallationId === isId,
      )

      const btm = isp?.batchtemplate?.batchTemplateMaterials.find(
        (btm) => btm.materialId === request.materialId,
      )

      result.set(key, btm || {})
    }

    return result
  }

  /**
   * Batch load analysis headers for multiple productions
   */
  private async _loadAnalysisHeadersBatch(
    productionIds: string[],
  ): Promise<Map<string, InstallationAnalysisHeader[]>> {
    if (productionIds.length === 0) return new Map()

    // Load productions with their analysis headers
    const productionsWithAnalysis = await this.prisma.batchProcessProduction.findMany({
      where: { id: { in: productionIds } },
      select: {
        id: true,
        batchProcess: {
          select: {
            processId: true,
            subinstallation: { select: { id: true } },
            batch: { select: { processtypeId: true } },
          },
        },
        batchProcessAnalyseHeader: {
          select: {
            id: true,
            dtanalysis: true,
            batchProcessSamplerequest: {
              select: { kind: { select: { value: { select: { name: true } } } } },
            },
            batchProcessAnalyseDetails: {
              select: {
                id: true,
                analyseContent: true,
                componentId: true,
                component: {
                  select: {
                    id: true,
                    name: true,
                    sampletypeComponents: {
                      select: {
                        processId: true,
                        processtypeId: true,
                        componentId: true,
                        mincontent: true,
                        maxcontent: true,
                        sequence: true,
                        unit: { select: { value: { select: { name: true } } } },
                      },
                    },
                  },
                },
                unit: { select: { value: { select: { name: true } } } },
              },
            },
            batchProcessProductions: { select: { id: true } },
            batchProcessAnalyseAttribute: {
              select: {
                value: true,
                type: { select: { value: { select: { name: true } } } },
              },
            },
          },
        },
      },
    })

    const result = new Map<string, InstallationAnalysisHeader[]>()

    for (const p of productionsWithAnalysis) {
      if (!p.batchProcessAnalyseHeader) {
        result.set(p.id, [])
        continue
      }

      try {
        const srKind = p.batchProcessAnalyseHeader?.batchProcessSamplerequest?.kind?.value?.name
        const ratioDefs = await this.analysisService.getRatioDefinitions(
          p.batchProcess.subinstallation.id,
          p.batchProcess.processId,
          srKind ?? '',
        )
        const ratios = await this.analysisService.populateRatiosForAnalysisHeader(
          ratioDefs,
          p.batchProcessAnalyseHeader as any,
        )

        const details = p.batchProcessAnalyseHeader.batchProcessAnalyseDetails
          .map((d) => {
            const stc = d.component.sampletypeComponents?.find(
              (stc) =>
                stc.processId === p.batchProcess.processId &&
                stc.processtypeId === p.batchProcess.batch?.processtypeId,
            )
            if (!stc) return null // hide details without SOPSOC
            const sopsocSpec = this.sopsoc.extractSpecFromRecord(stc)
            const analysisValue = d.analyseContent?.toNumber?.() ?? d.analyseContent
            const valueUnit = d.unit.value.name as any
            const evalResult = this.sopsoc.evaluate(analysisValue, valueUnit, sopsocSpec as any)
            return {
              id: d.id,
              name: d.component.name,
              value: d.analyseContent,
              unit: d.unit.value.name,
              minContent: stc?.mincontent ?? 0,
              maxContent: stc?.maxcontent ?? 0,
              sopsocUnit: sopsocSpec?.unit,
              outOfBounds: evalResult ? evalResult.outOfBounds : undefined,
              sequence: stc?.sequence ?? Number.MAX_SAFE_INTEGER,
            }
          })
          .filter((x): x is any => x !== null)
          .sort((a, b) => a.sequence - b.sequence)

        const analysisHeaders: InstallationAnalysisHeader[] = [
          {
            id: p.batchProcessAnalyseHeader.id,
            dtAnalysis: p.batchProcessAnalyseHeader.dtanalysis,
            ratios,
            batchProcessProductions: p.batchProcessAnalyseHeader.batchProcessProductions.length,
            sampleRequestAttributes: p.batchProcessAnalyseHeader.batchProcessAnalyseAttribute.map(
              (attr) => ({ value: attr.value, type: attr.type.value.name }),
            ),
            details,
          },
        ]

        result.set(p.id, analysisHeaders)
      } catch (e) {
        this.logger.warn(
          `Failed to compute analysis for production ${p.id}: ${(e as Error).message}`,
          p.id,
        )
        result.set(p.id, [])
      }
    }

    return result
  }
Editor is loading...
Leave a Comment