package com.ffii.fpsms.modules.report.service

import org.springframework.stereotype.Service
import net.sf.jasperreports.engine.*
import net.sf.jasperreports.engine.data.JRMapCollectionDataSource
import java.io.ByteArrayOutputStream
import java.io.InputStream
import com.ffii.core.support.JdbcDao
import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter
import net.sf.jasperreports.export.SimpleExporterInput
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput

@Service
open class ReportService(
    private val jdbcDao: JdbcDao,
) {
    /**
     * Queries the database for inventory data based on dates and optional item type.
     */
    fun searchReport1(fromDate: String, toDate: String, itemType: String?): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>(
            "fromDate" to fromDate, 
            "toDate" to toDate
        )
        
        val itemTypeSql = if (!itemType.isNullOrBlank()) {
            args["itemType"] = "%$itemType%"
            "AND it.type LIKE :itemType"
        } else ""

        val sql = """
            SELECT 
                -- fake value for demo
                8.8 * iv.onHandQty as stockValue,
                iv.onHandQty, 
                it.code as itemCode, 
                it.name as itemName, 
                uc.code as uom, 
                it.type, iv.status
            FROM inventory iv
            LEFT JOIN items it ON iv.itemId = it.id
            LEFT JOIN uom_conversion uc ON iv.uomId = uc.id
            WHERE iv.created >= :fromDate AND iv.created < :toDate
            $itemTypeSql
        """.trimIndent()
        
        return jdbcDao.queryForList(sql, args)
    }


    fun searchReport3( itemType: String?): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>(

        )
        
        val itemTypeSql = if (!itemType.isNullOrBlank()) {
            args["itemType"] = "%$itemType%"
            "AND it.type LIKE :itemType"
        } else ""

        val sql = """
            SELECT 
                -- fake value for demo
                8.8 * iv.onHandQty as stockValue,
                iv.onHandQty, 
                it.code as itemCode, 
                it.name as itemName, 
                uc.code as uom, 
                it.type, iv.status
            FROM inventory iv
            LEFT JOIN items it ON iv.itemId = it.id
            LEFT JOIN uom_conversion uc ON iv.uomId = uc.id
            WHERE iv.created >= :fromDate AND iv.created < :toDate
            $itemTypeSql
        """.trimIndent()
        
        return jdbcDao.queryForList(sql, args)
    }
    fun searchFGDeliveryReport(
        stockCategory: String?,
        stockSubCategory: String?,
        itemCode: String?,
        year: String?,
        lastOutDateStart: String?,
        lastOutDateEnd: String?
    ): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>()
        val stockCategorySql = ""
        val stockSubCategorySql = ""


        
        val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args)
        
        val yearSql = if (!year.isNullOrBlank()) {
            args["year"] = year
            "AND YEAR(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) = :year"
        } else {
            ""
        }
        
        val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) {
            val formattedDate = lastOutDateStart.replace("/", "-")
            args["lastOutDateStart"] = formattedDate
            "AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) >= DATE(:lastOutDateStart)"
        } else ""
        
        val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) {
            val formattedDate = lastOutDateEnd.replace("/", "-")
            args["lastOutDateEnd"] = formattedDate
            "AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) <= DATE(:lastOutDateEnd)"
        } else ""
    
        val sql = """
    SELECT
        IFNULL(DATE_FORMAT(
            IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate),
            '%Y-%m-%d'
        ), '')                                        AS deliveryDate,
        IFNULL(it.code, '')                           AS itemNo,
        IFNULL(it.name, '')                           AS itemName,
        IFNULL(uc.udfudesc, '')                       AS unitOfMeasure,
        IFNULL(dpor.deliveryNoteCode, '')             AS dnNo,
        CAST(IFNULL(sp.id, 0) AS CHAR)                AS customerId,
        IFNULL(sp.name, '')                           AS customerName,
   CAST(
    SUM(IFNULL(IFNULL(sol.qty, dol.qty), 0)) OVER (
        PARTITION BY it.code
    ) AS DECIMAL(14,2)
) AS qtyNumeric,

FORMAT(ROUND(IFNULL(IFNULL(sol.qty, dol.qty), 0), 0), 0) AS qty,
        COALESCE(
            dpor.TruckLanceCode,
            (SELECT t2.TruckLanceCode
             FROM truck t2
             WHERE t2.shopId = do.shopId
               AND t2.deleted = 0
               AND t2.Store_id = CASE 
                WHEN supplier.code = 'P06B' THEN '4F'
                WHEN supplier.code IN ('P07', 'P06D') THEN '2F'
                ELSE NULL 
                END
               AND (
                   (CASE 
                    WHEN supplier.code = 'P06B' THEN '4F'
                    WHEN supplier.code IN ('P07', 'P06D') THEN '2F'
                    ELSE NULL 
                    END
                    AND (SELECT COUNT(*) FROM truck t3 
                         WHERE t3.shopId = do.shopId AND t3.deleted = 0 
                         AND t3.Store_id = '4F') > 1
                    AND IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate) IS NOT NULL
                    AND t2.TruckLanceCode LIKE CONCAT('%', 
                        CASE DAYNAME(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate))
                            WHEN 'Monday' THEN 'Mon'
                            WHEN 'Tuesday' THEN 'Tue'
                            WHEN 'Wednesday' THEN 'Wed'
                            WHEN 'Thursday' THEN 'Thu'
                            WHEN 'Friday' THEN 'Fri'
                            WHEN 'Saturday' THEN 'Sat'
                            WHEN 'Sunday' THEN 'Sun'
                            ELSE ''
                        END, '%'))
                   OR
                   t2.Store_id = CASE 
                    WHEN supplier.code = 'P06B' THEN '4F'
                    WHEN supplier.code IN ('P07', 'P06D') THEN '2F'
                    ELSE NULL 
                    END
               )
             ORDER BY t2.DepartureTime ASC
             LIMIT 1),
            ''
        )                                             AS truckNo,
        ''                                            AS driver,
        IFNULL(do.code, '')                           AS deliveryOrderNo,
        IFNULL(qc.name, '')                            AS stockSubCategory
    FROM do_pick_order_line_record dpolr
    LEFT JOIN do_pick_order_record dpor
        ON dpolr.do_pick_order_id = dpor.record_id
       AND dpor.deleted = 0
       AND dpor.ticket_status = 'completed'
        INNER JOIN delivery_order do
            ON dpolr.do_order_id = do.id
           AND do.deleted = 0
        LEFT JOIN shop supplier
            ON do.supplierId = supplier.id
           AND supplier.deleted = 0
        LEFT JOIN shop sp
            ON do.shopId = sp.id
           AND sp.deleted = 0
        LEFT JOIN delivery_order_line dol
            ON do.id = dol.deliveryOrderId
           AND dol.deleted = 0
        LEFT JOIN items it
            ON dol.itemId = it.id
           AND it.deleted = 0
        LEFT JOIN items_qc_category_mapping iqcm
            ON it.id = iqcm.itemId
        LEFT JOIN qc_category qc
            ON iqcm.qcCategoryId = qc.id
           AND qc.deleted = 0
        LEFT JOIN item_category ic
            ON it.categoryId = ic.id
        LEFT JOIN item_uom iu
            ON it.id = iu.itemId
           AND iu.stockUnit = 1
        LEFT JOIN uom_conversion uc
            ON iu.uomId = uc.id
        LEFT JOIN pick_order_line pol
            ON dpolr.pick_order_id = pol.poId
           AND pol.itemId = it.id
           AND pol.deleted = 0
        LEFT JOIN stock_out_line sol
            ON pol.id = sol.pickOrderLineId
           AND sol.itemId = it.id
           AND sol.deleted = 0
           AND sol.status = 'completed'
        LEFT JOIN inventory_lot_line ill
            ON sol.inventoryLotLineId = ill.id
           AND ill.deleted = 0
        LEFT JOIN inventory_lot il
            ON ill.inventoryLotId = il.id
           AND il.deleted = 0
        LEFT JOIN stock_in_line sil
            ON il.stockInLineId = sil.id
           AND sil.deleted = 0
    WHERE
        dpolr.deleted = 0
        AND (dpor.id IS NULL OR dpor.ticket_status = 'completed')
        $stockCategorySql
        $stockSubCategorySql
        $itemCodeSql
        $yearSql
        $lastOutDateStartSql
        $lastOutDateEndSql
     ORDER BY
        it.code,
        deliveryDate,
        qc.name,
        IFNULL(sil.lotNo, '')
""".trimIndent()

val result = jdbcDao.queryForList(sql, args)
    
// 打印查询结果（前 10 行）
println("=== Query Result (Total: ${result.size} rows) ===")
result.take(50).forEachIndexed { index, row ->
    println("Row $index:")
    println("  deliveryDate: ${row["deliveryDate"]}")
    println("  itemNo: ${row["itemNo"]}")
    println("  itemName: ${row["itemName"]}")
    println("  qty: ${row["qty"]}")
    println("  qtyNumeric: ${row["qtyNumeric"]}")
    println("  deliveryOrderNo: ${row["deliveryOrderNo"]}")
    println("  dnNo: ${row["dnNo"]}")
    println("  stockSubCategory: ${row["stockSubCategory"]}")
    println("  ---")
}
if (result.size > 50) {
    println("... (showing first 10 rows, total ${result.size} rows)")
}

return result
     
    }

 fun getDistinctHandlersForFGStockOutTraceability(): List<String> {
    val sql = """
        SELECT DISTINCT COALESCE(picker_user.name, modified_user.name, '') AS handler
        FROM stock_out_line sol
        INNER JOIN stock_out so ON sol.stockOutId = so.id AND so.deleted = 0 AND so.type = 'do'
        LEFT JOIN user picker_user ON sol.handled_by = picker_user.id AND picker_user.deleted = 0
        LEFT JOIN user modified_user ON sol.modifiedBy = modified_user.staffNo AND modified_user.deleted = 0 AND sol.handled_by IS NULL
        WHERE sol.deleted = 0
        ORDER BY handler
    """.trimIndent()
    return jdbcDao.queryForList(sql, emptyMap<String, Any>()).map { row -> (row["handler"]?.toString() ?: "").trim() }.filter { it.isNotBlank() }
}

 fun getDistinctHandlersForMaterialStockOutTraceability(): List<String> {
    val sql = """
        SELECT DISTINCT COALESCE(picker_user.name, jpo_handler_user.name, created_user.name, modified_user.name, '') AS handler
        FROM stock_out_line sol
        INNER JOIN stock_out so ON sol.stockOutId = so.id AND so.deleted = 0 AND so.type = 'job'
        INNER JOIN pick_order_line pol ON sol.pickOrderLineId = pol.id AND pol.deleted = 0
        INNER JOIN pick_order po ON pol.poId = po.id AND po.deleted = 0 AND po.type IN ('jo', 'JOB_ORDER') AND po.joId IS NOT NULL
        LEFT JOIN user picker_user ON sol.handled_by = picker_user.id AND picker_user.deleted = 0
        LEFT JOIN jo_pick_order jpo ON po.id = jpo.pick_order_id AND pol.itemId = jpo.item_id AND jpo.deleted = 0
        LEFT JOIN user jpo_handler_user ON jpo.handled_by = jpo_handler_user.id AND jpo_handler_user.deleted = 0
        LEFT JOIN user created_user ON sol.createdBy = created_user.username AND created_user.deleted = 0
        LEFT JOIN user modified_user ON sol.modifiedBy = modified_user.staffNo AND modified_user.deleted = 0 AND sol.handled_by IS NULL
        WHERE sol.deleted = 0
        ORDER BY handler
    """.trimIndent()
    return jdbcDao.queryForList(sql, emptyMap<String, Any>()).map { row -> (row["handler"]?.toString() ?: "").trim() }.filter { it.isNotBlank() }
}

 fun searchFGStockOutTraceabilityReport(
    stockCategory: String?,
    stockSubCategory: String?,
    itemCode: String?,
    year: String?,
    lastOutDateStart: String?,
    lastOutDateEnd: String?,
    handler: String?
): List<Map<String, Any>> {
    val args = mutableMapOf<String, Any>()
    
    // Stock Category 过滤：通过 items.type
    val stockCategorySql = if (!stockCategory.isNullOrBlank()) {
        val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() }
        if (categories.isNotEmpty()) {
            val conditions = categories.mapIndexed { index, cat ->
                val paramName = "stockCategory_$index"
                args[paramName] = cat
                "it.type = :$paramName"
            }
            "AND (${conditions.joinToString(" OR ")})"
        } else {
            ""
        }
    } else {
        ""
    }
    
    // 移除 stockSubCategory 过滤（不需要）
    
    val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args)
    
    val yearSql = if (!year.isNullOrBlank()) {
        args["year"] = year
        "AND YEAR(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) = :year"
    } else {
        ""
    }
    
    val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) {
        val formattedDate = lastOutDateStart.replace("/", "-")
        args["lastOutDateStart"] = formattedDate
        "AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) >= DATE(:lastOutDateStart)"
    } else ""
    
    val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) {
        val formattedDate = lastOutDateEnd.replace("/", "-")
        args["lastOutDateEnd"] = formattedDate
        "AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) <= DATE(:lastOutDateEnd)"
    } else ""

    val handlerSql = buildMultiValueExactClause(
        handler,
        "COALESCE(picker_user.name, modified_user.name, '')",
        "handler",
        args
    )

    val sql = """
      SELECT
        IFNULL(DATE_FORMAT(
            IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate),
            '%Y-%m-%d'
        ), '') AS deliveryDate,
        IFNULL(it.code, '') AS itemNo,
        IFNULL(it.name, '') AS itemName,
        IFNULL(uc.udfudesc, '') AS unitOfMeasure,
        IFNULL(dpor.deliveryNoteCode, '') AS dnNo,
        CAST(IFNULL(sp.id, 0) AS CHAR) AS customerId,
        IFNULL(sp.name, '') AS customerName,
        FORMAT(
            ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0
        ) AS qtyNumeric,
        FORMAT(ROUND(IFNULL(sol.qty, 0), 0), 0) AS qty,
        '' AS truckNo,
        '' AS driver,
        IFNULL(do.code, '') AS deliveryOrderNo,
        IFNULL(po.code, '') AS fgPickOrderNo,
        IFNULL(po.code, '') AS stockReqNo,
        IFNULL(il.lotNo, '') AS lotNo,
        IFNULL(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate,
        FORMAT(ROUND(IFNULL(sol.qty, 0), 0), 0) AS stockOutQty,
        COALESCE(
            picker_user.name,
            modified_user.name,
            ''
        ) AS handler,
        COALESCE(
            picker_user.name,
            modified_user.name,
            ''
        ) AS pickedBy,
        GROUP_CONCAT(DISTINCT wh.code ORDER BY wh.code SEPARATOR ', ') AS storeLocation,
        '' AS pickRemark,
        FORMAT(
            ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0
        ) AS totalStockOutQty,
        0 AS stockSubCategory
    FROM do_pick_order_line_record dpolr
        LEFT JOIN do_pick_order_record dpor
            ON dpolr.record_id = dpor.id
           AND dpor.deleted = 0
           AND dpor.ticket_status = 'completed'
        INNER JOIN delivery_order do
            ON dpolr.do_order_id = do.id
           AND do.deleted = 0
        LEFT JOIN shop sp
            ON do.shopId = sp.id
           AND sp.deleted = 0
        LEFT JOIN delivery_order_line dol
            ON do.id = dol.deliveryOrderId
           AND dol.deleted = 0
        LEFT JOIN items it
            ON dol.itemId = it.id
           AND it.deleted = 0
        LEFT JOIN item_uom iu
            ON it.id = iu.itemId
           AND iu.stockUnit = 1
        LEFT JOIN uom_conversion uc
            ON iu.uomId = uc.id
        LEFT JOIN pick_order_line pol
            ON dpolr.pick_order_id = pol.poId
           AND pol.itemId = it.id
           AND pol.deleted = 0
        LEFT JOIN pick_order po
            ON pol.poId = po.id
           AND po.deleted = 0
               LEFT JOIN stock_out_line sol
            ON pol.id = sol.pickOrderLineId
           AND sol.itemId = it.id
           AND sol.deleted = 0
        LEFT JOIN stock_out so
            ON sol.stockOutId = so.id
           AND so.deleted = 0
           AND so.type = 'do'
        LEFT JOIN inventory_lot_line ill
            ON sol.inventoryLotLineId = ill.id
           AND ill.deleted = 0
        LEFT JOIN inventory_lot il
            ON ill.inventoryLotId = il.id
           AND il.deleted = 0
        LEFT JOIN warehouse wh
            ON ill.warehouseId = wh.id
           AND wh.deleted = 0
        LEFT JOIN user picker_user
            ON sol.handled_by = picker_user.id
           AND picker_user.deleted = 0
        LEFT JOIN user modified_user
            ON sol.modifiedBy = modified_user.staffNo
           AND modified_user.deleted = 0
           AND sol.handled_by IS NULL
    WHERE
        dpolr.deleted = 0
        $stockCategorySql
        $itemCodeSql
        $yearSql
        $lastOutDateStartSql
        $lastOutDateEndSql
        $handlerSql
        GROUP BY
        sol.id,
        dpor.RequiredDeliveryDate,
        do.estimatedArrivalDate,
        it.code,
        it.name,
        uc.udfudesc,
        dpor.deliveryNoteCode,
        sp.id,
        sp.name,
        sol.qty,
        picker_user.name,
        modified_user.name,
        po.code,
        do.code,
        il.lotNo,
        il.expiryDate
    ORDER BY
        it.code,
        deliveryDate,
        il.lotNo
""".trimIndent()

    val result = jdbcDao.queryForList(sql, args)
    
    // 打印查询结果
    println("=== Query Result (Total: ${result.size} rows) ===")
    result.take(50).forEachIndexed { index, row ->
        println("Row $index:")
        println("  deliveryDate: ${row["deliveryDate"]}")
        println("  itemNo: ${row["itemNo"]}")
        println("  itemName: ${row["itemName"]}")
        println("  qty: ${row["qty"]}")
        println("  qtyNumeric: ${row["qtyNumeric"]}")
        println("  deliveryOrderNo: ${row["deliveryOrderNo"]}")
        println("  dnNo: ${row["dnNo"]}")
        println("  fgPickOrderNo: ${row["fgPickOrderNo"]}")
        println("  pickedBy: ${row["pickedBy"]}")
        println("  storeLocation: ${row["storeLocation"]}")
        println("  ---")
    }
    if (result.size > 50) {
        println("... (showing first 50 rows, total ${result.size} rows)")
    }

    return result
}
fun searchMaterialStockOutTraceabilityReport(
    stockCategory: String?,
    stockSubCategory: String?,
    itemCode: String?,
    year: String?,
    lastOutDateStart: String?,
    lastOutDateEnd: String?,
    handler: String?
): List<Map<String, Any>> {
    val args = mutableMapOf<String, Any>()

    // Stock Category 过滤：通过 items.type
    val stockCategorySql = buildMultiValueExactClause(
        stockCategory,
        "it.type",
        "stockCategory",
        args
    )

    val stockSubCategorySql = ""

    val itemCodeSql = buildMultiValueLikeClause(
        itemCode,
        "it.code",
        "itemCode",
        args
    )

    // 年份过滤：使用 sol.endTime 的年份
    val yearSql = if (!year.isNullOrBlank()) {
        args["year"] = year
        "AND YEAR(sol.endTime) = :year"
    } else {
        ""
    }

    val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) {
        val formattedDate = lastOutDateStart.replace("/", "-")
        args["lastOutDateStart"] = formattedDate
        "AND DATE(sol.endTime) >= DATE(:lastOutDateStart)"
    } else {
        ""
    }

    val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) {
        val formattedDate = lastOutDateEnd.replace("/", "-")
        args["lastOutDateEnd"] = formattedDate
        "AND DATE(sol.endTime) <= DATE(:lastOutDateEnd)"
    } else {
        ""
    }

    val handlerSql = buildMultiValueExactClause(
        handler,
        "COALESCE(picker_user.name, jpo_handler_user.name, created_user.name, modified_user.name, '')",
        "handler",
        args
    )

    val sql = """
          SELECT
         IFNULL(it.code, '')                                AS itemNo,
         IFNULL(it.name, '')                                AS itemName,
         IFNULL(it.categoryId, 0)                           AS stockSubCategory,
         IFNULL(uc.udfudesc, '')                            AS unitOfMeasure,
         IFNULL(jo.code, '')                               AS jobOrderNo,
         IFNULL(po.consoCode, '')                           AS stockReqNo,
         IFNULL(il.lotNo, '')                               AS lotNo,
         IFNULL(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate,
         FORMAT(ROUND(IFNULL(sol.qty, 0), 0), 0)           AS stockOutQty,
         IFNULL(po.code, '')                                AS materialPickOrderNo,
         COALESCE(
             picker_user.name,
             jpo_handler_user.name,
             created_user.name,
             modified_user.name,
             ''
         )                                                  AS handler,
         COALESCE(wh.code, '')                              AS storeLocation,
         ''                                                 AS pickRemark,
         FORMAT(
             ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0
         )                                                  AS totalStockOutQty
         FROM stock_out_line sol
         INNER JOIN stock_out so
             ON sol.stockOutId = so.id
            AND so.deleted = 0
            AND so.type = 'job'
         INNER JOIN pick_order_line pol
             ON sol.pickOrderLineId = pol.id
            AND pol.deleted = 0
         INNER JOIN pick_order po
             ON pol.poId = po.id
            AND po.deleted = 0
            AND po.type IN ('jo', 'JOB_ORDER')
            AND po.joId IS NOT NULL
         LEFT JOIN job_order jo
             ON po.joId = jo.id
            AND jo.deleted = 0
         INNER JOIN items it
             ON sol.itemId = it.id
            AND it.deleted = 0
         LEFT JOIN item_uom iu
             ON it.id = iu.itemId
            AND iu.stockUnit = 1
         LEFT JOIN uom_conversion uc
             ON iu.uomId = uc.id
         LEFT JOIN inventory_lot_line ill
             ON sol.inventoryLotLineId = ill.id
            AND ill.deleted = 0
         LEFT JOIN inventory_lot il
             ON ill.inventoryLotId = il.id
            AND il.deleted = 0
         LEFT JOIN warehouse wh
             ON ill.warehouseId = wh.id
            AND wh.deleted = 0
         LEFT JOIN user picker_user
             ON sol.handled_by = picker_user.id
            AND picker_user.deleted = 0
         LEFT JOIN jo_pick_order jpo
             ON po.id = jpo.pick_order_id
            AND pol.itemId = jpo.item_id
            AND jpo.deleted = 0
                  LEFT JOIN user jpo_handler_user
             ON jpo.handled_by = jpo_handler_user.id
            AND jpo_handler_user.deleted = 0
         LEFT JOIN user created_user
             ON sol.createdBy = created_user.username   
            AND created_user.deleted = 0
         LEFT JOIN user modified_user
             ON sol.modifiedBy = modified_user.staffNo
            AND modified_user.deleted = 0
            AND sol.handled_by IS NULL
            WHERE
         sol.deleted = 0
         AND (sol.inventoryLotLineId IS NULL OR ill.id IS NOT NULL)
         $stockCategorySql
         $stockSubCategorySql
         $itemCodeSql
         $yearSql
         $lastOutDateStartSql
         $lastOutDateEndSql
         $handlerSql
     ORDER BY
         it.code,
         il.lotNo,
         sol.endTime
 """.trimIndent()
    val result = jdbcDao.queryForList(sql, args)

    println("=== Material Stock Out Traceability (Total: ${result.size} rows) ===")
    result.take(50).forEachIndexed { index, row ->
        println("Row $index:")
        println("  itemNo: ${row["itemNo"]}")
        println("  itemName: ${row["itemName"]}")
        println("  jobOrderNo: ${row["jobOrderNo"]}")
        println("  stockOutQty: ${row["stockOutQty"]}")
        println("  totalStockOutQty: ${row["totalStockOutQty"]}")
        println("  materialPickOrderNo: ${row["materialPickOrderNo"]}")
        println("  handler: ${row["handler"]}")
        println("  storeLocation: ${row["storeLocation"]}")
        println("  ---")
    }
    if (result.size > 50) {
        println("... (showing first 50 rows, total ${result.size} rows)")
    }

    return result
}
    /**
     * Helper function to build SQL clause for comma-separated values.
     * Supports multiple values like "val1, val2, val3" and generates OR conditions with LIKE.
     */
    private fun buildMultiValueLikeClause(
        paramValue: String?,
        columnName: String,
        paramPrefix: String,
        args: MutableMap<String, Any>
    ): String {
        if (paramValue.isNullOrBlank()) return ""
        
        val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() }
        if (values.isEmpty()) return ""
        
        val conditions = values.mapIndexed { index, value ->
            val paramName = "${paramPrefix}_$index"
            args[paramName] = "%$value%"
            "$columnName LIKE :$paramName"
        }
        
        return "AND (${conditions.joinToString(" OR ")})"
    }

    /**
     * Helper function to build SQL clause for comma-separated values with exact match.
     * Supports multiple values like "val1, val2, val3" and generates OR conditions with =.
     */
    private fun buildMultiValueExactClause(
        paramValue: String?,
        columnName: String,
        paramPrefix: String,
        args: MutableMap<String, Any>
    ): String {
        if (paramValue.isNullOrBlank()) return ""
        
        val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() }
        if (values.isEmpty()) return ""
        
        val conditions = values.mapIndexed { index, value ->
            val paramName = "${paramPrefix}_$index"
            args[paramName] = value
            "$columnName = :$paramName"
        }
        
        return "AND (${conditions.joinToString(" OR ")})"
    }

    /**
     * Queries the database for Stock In Traceability Report data.
     * Joins stock_in_line, stock_in, items, qc_result, inventory_lot, inventory_lot_line, warehouse, and shop tables.
     * Supports comma-separated values for stockCategory (items.type) and itemCode.
     */
    fun searchStockInTraceabilityReport(
        stockCategory: String?,
        itemCode: String?,
        lastInDateStart: String?,
        lastInDateEnd: String?
    ): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>()
        

        // If "All" is selected or contains "All", don't filter by stock category
        val stockCategorySql = if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) {
            buildMultiValueExactClause(stockCategory, "it.type", "stockCategory", args)
        } else {
            ""
        }
        val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args)
        
        val lastInDateStartSql = if (!lastInDateStart.isNullOrBlank()) {
            val formattedDate = lastInDateStart.replace("/", "-")
            args["lastInDateStart"] = formattedDate
            "AND DATE(sil.receiptDate) >= DATE(:lastInDateStart)"
        } else ""
        
        val lastInDateEndSql = if (!lastInDateEnd.isNullOrBlank()) {
            val formattedDate = lastInDateEnd.replace("/", "-")
            args["lastInDateEnd"] = formattedDate
            "AND DATE(sil.receiptDate) <= DATE(:lastInDateEnd)"
        } else ""

        val sql = """
        SELECT
            COALESCE(it.code, '') as itemNo,
            COALESCE(it.name, '') as itemName,
            COALESCE(uc.udfudesc, '') as unitOfMeasure,
            COALESCE(sil.dnNo, '') as dnNo,
            COALESCE(sil.lotNo, il.lotNo, '') as lotNo,
            COALESCE(DATE_FORMAT(COALESCE(sil.expiryDate, il.expiryDate), '%Y-%m-%d'), '') as expiryDate,
            CASE WHEN COALESCE(qr_agg.qcFailed, 0) = 1 THEN '0'
                 ELSE TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(sil.acceptedQty, 0), 2)))
            END as stockInQty,
            TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(sil.acceptedQty, 0), 2))) as iqcSampleQty,
            TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(qr_agg.failQtySum, 0), 2))) as iqcDefectQty,
            TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(
                CASE WHEN COALESCE(sil.acceptedQty, 0) > 0
                     THEN COALESCE(qr_agg.failQtySum, 0) / sil.acceptedQty * 100
                     ELSE 0
                END, 2))) as iqcDefectPercentage,
            CASE WHEN COALESCE(qr_agg.qcFailed, 0) = 1 THEN '不合格' ELSE '已接受' END as iqcResult,
            COALESCE(qr_agg.remarksConcat, '') as iqcRemarks,
            TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(qr_agg.failQtySum, 0)) OVER (PARTITION BY it.id), 2))) as totalIqcDefectQty,
            COALESCE(wh.code, '') as storeLocation,
            COALESCE(sp_si.code, sp_po.code, '') as supplierID,
            COALESCE(sp_si.name, sp_po.name, '') as supplierName,
            TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(sil.acceptedQty, 0)) OVER (PARTITION BY it.id), 2))) as totalStockInQty,
            TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(sil.acceptedQty, 0)) OVER (PARTITION BY it.id), 2))) as totalIqcSampleQty
        FROM stock_in_line sil
        LEFT JOIN stock_in si ON sil.stockInId = si.id
        LEFT JOIN purchase_order po ON sil.purchaseOrderId = po.id
        LEFT JOIN items it ON sil.itemId = it.id
        LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = true
        LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
        LEFT JOIN inventory_lot il ON sil.inventoryLotId = il.id
        LEFT JOIN inventory_lot_line ill ON il.id = ill.inventoryLotId
        LEFT JOIN warehouse wh ON ill.warehouseId = wh.id
        LEFT JOIN shop sp_si ON si.supplierId = sp_si.id
        LEFT JOIN shop sp_po ON po.supplierId = sp_po.id
        LEFT JOIN (
            SELECT
                qr.stockInLineId,
                SUM(COALESCE(qr.failQty, 0)) AS failQtySum,
                MAX(CASE WHEN qr.qcPassed = 0 THEN 1 ELSE 0 END) AS qcFailed,
                GROUP_CONCAT(
                    CASE WHEN qr.remarks IS NOT NULL AND TRIM(qr.remarks) != '' THEN qr.remarks END
                    SEPARATOR '; '
                ) AS remarksConcat
            FROM qc_result qr
            WHERE qr.deleted = 0
            GROUP BY qr.stockInLineId
        ) qr_agg ON qr_agg.stockInLineId = sil.id
        WHERE sil.deleted = false
        $stockCategorySql
        $itemCodeSql
        $lastInDateStartSql
        $lastInDateEndSql
        ORDER BY it.code, sil.lotNo
    """.trimIndent()
        
        return jdbcDao.queryForList(sql, args)
    }

      /**
     * Queries the database for Stock Balance Report data (one summarized row per item).
     * Uses stock_ledger with report period (fromDate/toDate): opening = before fromDate, cum in/out = in period, current = up to toDate.
     * totalStockBalance = SUM(pol.qty * pol.up) per item via purchase_order_line; avgUnitPrice = totalStockBalance / totalCurrentBalance.
     */
    fun searchStockBalanceReport(
        stockCategory: String?,
        itemCode: String?,
        balanceFilterStart: String?,
        balanceFilterEnd: String?,
        storeLocation: String?,
        lastInDateStart: String?,
        lastInDateEnd: String?,
        lastOutDateStart: String?,
        lastOutDateEnd: String?,
        reportPeriodStart: String? = null,
        reportPeriodEnd: String? = null
    ): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>()
        val fromDate = reportPeriodStart?.replace("/", "-")?.takeIf { it.isNotBlank() }
            ?: java.time.LocalDate.now().withDayOfYear(1).toString()
        val toDate = reportPeriodEnd?.replace("/", "-")?.takeIf { it.isNotBlank() }
            ?: java.time.LocalDate.now().toString()
        args["fromDate"] = fromDate
        args["toDate"] = toDate

        val stockCategorySql = buildMultiValueExactClause(stockCategory, "it.type", "stockCategory", args)
        val itemCodeSql = buildMultiValueLikeClause(itemCode, "sl.itemCode", "itemCode", args)
        val storeLocationSql = if (!storeLocation.isNullOrBlank()) {
            args["storeLocation"] = "%$storeLocation%"
            "AND agg.storeLocation LIKE :storeLocation"
        } else ""

        val sql = """
            SELECT
                '' as stockSubCategory,
                COALESCE(item_agg.itemNo, '') as itemNo,
                COALESCE(item_agg.itemName, '') as itemName,
                COALESCE(item_agg.unitOfMeasure, '') as unitOfMeasure,
                '' as lotNo,
                '' as expiryDate,
                '' as openingBalance,
                '' as cumStockIn,
                '' as cumStockOut,
                '' as currentBalance,
                '' as reOrderQty,
                COALESCE(item_agg.storeLocation, '') as storeLocation,
                COALESCE(DATE_FORMAT(item_agg.lastInDate, '%Y-%m-%d'), '') as lastInDate,
                COALESCE(DATE_FORMAT(item_agg.lastOutDate, '%Y-%m-%d'), '') as lastOutDate,
                CASE WHEN COALESCE(item_agg.totalOpeningBalance, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalOpeningBalance, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalOpeningBalance, 0), 0) END as totalOpeningBalance,
                CASE WHEN COALESCE(item_agg.totalCumStockIn, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalCumStockIn, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalCumStockIn, 0), 0) END as totalCumStockIn,
                CASE WHEN COALESCE(item_agg.totalCumStockOut, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalCumStockOut, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalCumStockOut, 0), 0) END as totalCumStockOut,
                CASE WHEN COALESCE(item_agg.totalCurrentBalance, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalCurrentBalance, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalCurrentBalance, 0), 0) END as totalCurrentBalance,
                '' as misInputAndLost,
                '' as defectiveGoods,
                '' as variance,
                CASE WHEN COALESCE(item_agg.totalMisInputAndLost, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalMisInputAndLost, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalMisInputAndLost, 0), 0) END as totalMisInputAndLost,
                CASE WHEN COALESCE(item_agg.totalVariance, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalVariance, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalVariance, 0), 0) END as totalVariance,
                CASE WHEN COALESCE(item_agg.totalDefectiveGoods, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalDefectiveGoods, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalDefectiveGoods, 0), 0) END as totalDefectiveGoods,
                FORMAT(ROUND(COALESCE(ipv.totalStockBalanceRaw, 0), 2), 2) as totalStockBalance,
                CASE
                    WHEN COALESCE(item_agg.totalCurrentBalance, 0) > 0 AND ipv.totalStockBalanceRaw IS NOT NULL AND ipv.totalStockBalanceRaw <> 0
                    THEN FORMAT(ROUND(ipv.totalStockBalanceRaw / item_agg.totalCurrentBalance, 2), 2)
                    ELSE '0.00'
                END as avgUnitPrice
            FROM (
                SELECT
                    agg.itemCode AS itemNo,
                    agg.itemId,
                    it.name AS itemName,
                    COALESCE(uc.udfudesc, uc.code, '') AS unitOfMeasure,
                    SUM(agg.openingBalance) AS totalOpeningBalance,
                    SUM(agg.cumStockIn) AS totalCumStockIn,
                    SUM(agg.cumStockOut) AS totalCumStockOut,
                    SUM(agg.currentBalance) AS totalCurrentBalance,
                    SUM(agg.cumStockOutMiss) AS totalMisInputAndLost,
                    SUM(agg.cumStockOutAdjStockTake) AS totalVariance,
                    SUM(agg.cumStockOutBad) AS totalDefectiveGoods,
                    MAX(agg.storeLocation) AS storeLocation,
                    MAX(agg.lastInDate) AS lastInDate,
                    MAX(agg.lastOutDate) AS lastOutDate
                FROM (
                    SELECT
                        sl.itemCode,
                        sl.itemId,
                        COALESCE(il_in.id, il_out.id) AS lotId,
                        SUM(CASE WHEN DATE(sl.date) < :fromDate THEN COALESCE(sl.inQty, 0) - COALESCE(sl.outQty, 0) ELSE 0 END) AS openingBalance,
                        SUM(
                            CASE
                                WHEN DATE(sl.date) BETWEEN :fromDate AND :toDate
                                     AND sil.stockTakeLineId IS NULL
                                THEN COALESCE(sl.inQty, 0)
                                ELSE 0
                            END
                        ) AS cumStockIn,
                        SUM(
                            CASE
                                WHEN DATE(sl.date) BETWEEN :fromDate AND :toDate
                                     AND COALESCE(sl.outQty, 0) > 0
                                     AND NOT (
                                         LOWER(TRIM(COALESCE(sl.type, ''))) = 'stocktake'
                                         OR (
                                             LOWER(TRIM(COALESCE(sl.type, ''))) = 'adj'
                                             AND (sol.stockTransferId IS NULL OR sol.id IS NULL)
                                         )
                                         OR LOWER(TRIM(COALESCE(sl.type, ''))) = 'miss'
                                         OR LOWER(TRIM(COALESCE(sol.type, ''))) = 'miss'
                                         OR LOWER(TRIM(COALESCE(sl.type, ''))) = 'bad'
                                         OR LOWER(TRIM(COALESCE(sol.type, ''))) = 'bad'
                                     )
                                THEN COALESCE(sl.outQty, 0)
                                ELSE 0
                            END
                        ) AS cumStockOut,
                        SUM(CASE WHEN DATE(sl.date) <= :toDate THEN COALESCE(sl.inQty, 0) - COALESCE(sl.outQty, 0) ELSE 0 END) AS currentBalance,
                        MAX(CASE WHEN COALESCE(sl.inQty, 0) > 0 THEN sl.date END) AS lastInDate,
                        MAX(CASE WHEN COALESCE(sl.outQty, 0) > 0 THEN sl.date END) AS lastOutDate,
                        MAX(lot_wh.storeLocation) AS storeLocation,
                        SUM(CASE WHEN DATE(sl.date) BETWEEN :fromDate AND :toDate AND COALESCE(sl.inQty, 0) > 0 AND (LOWER(TRIM(COALESCE(sl.type, ''))) = 'miss' OR LOWER(TRIM(COALESCE(sol.type, ''))) = 'miss') THEN sl.outQty ELSE 0 END) AS cumStockOutMiss,
                        SUM(CASE WHEN DATE(sl.date) BETWEEN :fromDate AND :toDate AND COALESCE(sl.outQty, 0) > 0 AND (LOWER(TRIM(COALESCE(sl.type, ''))) = 'bad' OR LOWER(TRIM(COALESCE(sol.type, ''))) = 'bad') THEN sl.outQty ELSE 0 END) AS cumStockOutBad,
                        SUM(CASE WHEN DATE(sl.date) BETWEEN :fromDate AND :toDate AND COALESCE(sl.outQty, 0) > 0 AND LOWER(TRIM(COALESCE(sl.type, ''))) = 'adj' AND (sol.stockTransferId IS NULL OR sol.id IS NULL) THEN sl.outQty ELSE 0 END) AS cumStockOutAdjStockTake
                    FROM stock_ledger sl
                    LEFT JOIN stock_in_line sil ON sl.stockInLineId = sil.id AND sil.deleted = 0
                    LEFT JOIN inventory_lot il_in ON sil.inventoryLotId = il_in.id AND il_in.deleted = 0
                    LEFT JOIN stock_out_line sol ON sl.stockOutLineId = sol.id AND sol.deleted = 0
                    LEFT JOIN inventory_lot_line ill_out ON sol.inventoryLotLineId = ill_out.id AND ill_out.deleted = 0
                    LEFT JOIN inventory_lot il_out ON ill_out.inventoryLotId = il_out.id AND il_out.deleted = 0
                    LEFT JOIN (
                        SELECT il.id AS lotId, MAX(wh.code) AS storeLocation
                        FROM inventory_lot il
                        LEFT JOIN inventory_lot_line ill ON ill.inventoryLotId = il.id AND ill.deleted = 0
                        LEFT JOIN warehouse wh ON ill.warehouseId = wh.id AND wh.deleted = 0
                        GROUP BY il.id
                    ) lot_wh ON lot_wh.lotId = COALESCE(il_in.id, il_out.id)
                    WHERE sl.deleted = 0
                      AND sl.itemCode IS NOT NULL AND sl.itemCode <> ''
                      AND DATE(sl.date) <= :toDate
                      AND COALESCE(il_in.id, il_out.id) IS NOT NULL
                      $itemCodeSql
                    GROUP BY sl.itemCode, sl.itemId, COALESCE(il_in.id, il_out.id)
                ) agg
                LEFT JOIN items it ON agg.itemId = it.id AND it.deleted = 0
                LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = 1 AND iu.deleted = 0
                LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
                WHERE 1 = 1
                $stockCategorySql
                $storeLocationSql
                GROUP BY agg.itemCode, agg.itemId, it.id, it.code, it.name, uc.udfudesc, uc.code
                HAVING 1=1
            ) item_agg
            LEFT JOIN (
                SELECT itemId, SUM(pol_qty_up) AS totalStockBalanceRaw
                FROM (
                    SELECT sil.itemId, MAX(COALESCE(pol.qty, 0) * COALESCE(pol.up, 0)) AS pol_qty_up
                    FROM stock_in_line sil
                    LEFT JOIN purchase_order_line pol ON sil.purchaseOrderLineId = pol.id AND pol.deleted = 0
                    WHERE sil.deleted = 0
                    GROUP BY sil.itemId, pol.id
                ) t
                GROUP BY itemId
            ) ipv ON item_agg.itemId = ipv.itemId
            WHERE 1=1
        """.trimIndent()

        val havingConditions = mutableListOf<String>()
        val currentBalanceExpr = "COALESCE(totalCurrentBalance, 0)"

        if (!balanceFilterStart.isNullOrBlank()) {
            args["balanceFilterStart"] = balanceFilterStart.toDoubleOrNull() ?: 0.0
            havingConditions.add("$currentBalanceExpr >= :balanceFilterStart")
        }
        if (!balanceFilterEnd.isNullOrBlank()) {
            args["balanceFilterEnd"] = balanceFilterEnd.toDoubleOrNull() ?: 0.0
            havingConditions.add("$currentBalanceExpr <= :balanceFilterEnd")
        }
        if (!lastInDateStart.isNullOrBlank()) {
            val formattedDate = lastInDateStart.replace("/", "-")
            args["lastInDateStart"] = formattedDate
            havingConditions.add("(lastInDate IS NOT NULL AND DATE(lastInDate) >= DATE(:lastInDateStart))")
        }
        if (!lastInDateEnd.isNullOrBlank()) {
            val formattedDate = lastInDateEnd.replace("/", "-")
            args["lastInDateEnd"] = formattedDate
            havingConditions.add("(lastInDate IS NOT NULL AND DATE(lastInDate) <= DATE(:lastInDateEnd))")
        }
        if (!lastOutDateStart.isNullOrBlank()) {
            val formattedDate = lastOutDateStart.replace("/", "-")
            args["lastOutDateStart"] = formattedDate
            havingConditions.add("(lastOutDate IS NOT NULL AND DATE(lastOutDate) >= DATE(:lastOutDateStart))")
        }
        if (!lastOutDateEnd.isNullOrBlank()) {
            val formattedDate = lastOutDateEnd.replace("/", "-")
            args["lastOutDateEnd"] = formattedDate
            havingConditions.add("(lastOutDate IS NOT NULL AND DATE(lastOutDate) <= DATE(:lastOutDateEnd))")
        }

        val finalSql = if (havingConditions.isNotEmpty()) {
            sql.replace("HAVING 1=1", "HAVING ${havingConditions.joinToString(" AND ")}")
        } else {
            sql.replace("GROUP BY agg.itemCode, agg.itemId, it.id, it.code, it.name, uc.udfudesc, uc.code\n                HAVING 1=1", "GROUP BY agg.itemCode, agg.itemId, it.id, it.code, it.name, uc.udfudesc, uc.code")
        }
        return jdbcDao.queryForList("$finalSql ORDER BY itemNo", args)
    }
    /**
     * Compiles and fills a Jasper Report, then exports to Excel (.xlsx). Same layout/columns as the report template.
     */
    fun createExcelResponse(templatePath: String, params: Map<String, Any>, dataList: List<Map<String, Any>>): ByteArray {
        val stream = this::class.java.getResourceAsStream(templatePath)
            ?: throw RuntimeException("Report template not found: $templatePath")
        val jasperReport = JasperCompileManager.compileReport(stream)
        val dataSource = JRMapCollectionDataSource(dataList)
        val jasperPrint = JasperFillManager.fillReport(jasperReport, params, dataSource)
        val out = ByteArrayOutputStream()
        val exporter = JRXlsxExporter()
        exporter.setExporterInput(SimpleExporterInput(jasperPrint))
        exporter.setExporterOutput(SimpleOutputStreamExporterOutput(out))
        exporter.exportReport()
        return out.toByteArray()
    }

    /**
     * Compiles and fills a Jasper Report, returning the PDF as a ByteArray.
     */
    fun createPdfResponse(templatePath: String, params: Map<String, Any>, dataList: List<Map<String, Any>>): ByteArray {
        val stream = this::class.java.getResourceAsStream(templatePath)
            ?: throw RuntimeException("Report template not found: $templatePath")
            
        val jasperReport = JasperCompileManager.compileReport(stream)
        
        val dataSource = JRMapCollectionDataSource(dataList)
        
        val jasperPrint = JasperFillManager.fillReport(jasperReport, params, dataSource)
        return JasperExportManager.exportReportToPdf(jasperPrint)
    }
}