diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt index c57fc55..55ef7d3 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt @@ -97,6 +97,30 @@ open class ReportService( 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 { + 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, item_category, qc_result, inventory_lot, inventory_lot_line, warehouse, and shop tables. @@ -106,6 +130,7 @@ open class ReportService( stockCategory: String?, stockSubCategory: String?, itemCode: String?, + year: String?, lastInDateStart: String?, lastInDateEnd: String? ): List> { @@ -115,6 +140,13 @@ open class ReportService( val stockSubCategorySql = buildMultiValueLikeClause(stockSubCategory, "ic.sub", "stockSubCategory", args) val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args) + val yearSql = if (!year.isNullOrBlank()) { + args["year"] = year + "AND YEAR(sil.receiptDate) = :year" + } else { + "" + } + val lastInDateStartSql = if (!lastInDateStart.isNullOrBlank()) { args["lastInDateStart"] = lastInDateStart "AND sil.receiptDate >= :lastInDateStart" @@ -168,6 +200,7 @@ open class ReportService( $stockCategorySql $stockSubCategorySql $itemCodeSql + $yearSql $lastInDateStartSql $lastInDateEndSql ORDER BY ic.sub, it.code, sil.lotNo @@ -176,6 +209,205 @@ open class ReportService( return jdbcDao.queryForList(sql, args) } + /** + * Queries the database for Semi FG Production Analysis Report data. + * Flow: + * 1. Filter bom by description (FG/WIP) to get bom.code values + * 2. Match bom.code with stock_ledger.itemCode + * 3. Aggregate stock_ledger data by month for each item based on inQty + * Supports comma-separated values for stockCategory, stockSubCategory, and itemCode. + */ + fun searchSemiFGProductionAnalysisReport( + stockCategory: String?, + stockSubCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String? + ): List> { + val args = mutableMapOf() + + // Filter by stockCategory from bom.description (FG/WIP) - this finds which bom.code values match + // Supports multiple categories separated by comma (e.g., "FG,WIP") + // If "All" is selected or contains "All", don't filter by description + val stockCategorySql = if (!itemCode.isNullOrBlank()) { + // When itemCode is provided, skip stockCategory filter + "" + } else if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) { + // Handle multiple categories (comma-separated) + val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" } + if (categories.isNotEmpty()) { + val conditions = categories.mapIndexed { index, cat -> + val paramName = "stockCategory_$index" + args[paramName] = cat + "b.description = :$paramName" + } + "AND (${conditions.joinToString(" OR ")})" + } else { + "" + } + } else { + "" + } + val stockSubCategorySql = buildMultiValueLikeClause(stockSubCategory, "ic.sub", "stockSubCategory", args) + // Filter by itemCode - match bom.code (user input should match bom.code, which then matches stock_ledger.itemCode) + val itemCodeSql = buildMultiValueExactClause(itemCode, "b.code", "itemCode", args) + + val yearSql = if (!year.isNullOrBlank()) { + args["year"] = year + "AND YEAR(sl.modified) = :year" + } else { + "" + } + + val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) { + args["lastOutDateStart"] = lastOutDateStart + "AND DATE(sl.modified) >= :lastOutDateStart" + } else "" + + val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) { + args["lastOutDateEnd"] = lastOutDateEnd + "AND DATE(sl.modified) < :lastOutDateEnd" + } else "" + + val sql = """ + SELECT + COALESCE(ic.sub, '') as stockSubCategory, + COALESCE(sl.itemCode, '') as itemNo, + COALESCE(b.name, '') as itemName, + COALESCE(uc.code, '') as unitOfMeasure, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 1 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJan, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 2 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyFeb, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 3 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyMar, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 4 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyApr, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 5 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyMay, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 6 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJun, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 7 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJul, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 8 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyAug, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 9 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtySep, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 10 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyOct, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 11 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyNov, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 12 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyDec, + CAST(COALESCE(SUM(sl.inQty), 0) AS CHAR) as totalProductionQty + FROM stock_ledger sl + INNER JOIN bom b ON sl.itemCode = b.code AND b.deleted = false + LEFT JOIN items it ON sl.itemId = it.id + LEFT JOIN item_category ic ON it.categoryId = ic.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 + WHERE sl.deleted = false + AND sl.inQty IS NOT NULL + AND sl.inQty > 0 + $stockCategorySql + $stockSubCategorySql + $itemCodeSql + $yearSql + $lastOutDateStartSql + $lastOutDateEndSql + GROUP BY sl.itemCode, ic.sub, it.id, b.name, uc.code, b.description + ORDER BY ic.sub, sl.itemCode + """.trimIndent() + + return jdbcDao.queryForList(sql, args) + } + + /** + * Gets list of item codes (bom.code) with names based on stockCategory filter. + * Supports multiple categories separated by comma (e.g., "FG,WIP"). + * If stockCategory is "All" or null, returns all codes. + * If stockCategory is "FG" or "WIP" or "FG,WIP", returns codes matching those descriptions. + * Returns a list of maps with "code" and "name" keys. + */ + fun getSemiFGItemCodes(stockCategory: String?): List> { + val args = mutableMapOf() + + val stockCategorySql = if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) { + // Handle multiple categories (comma-separated) + val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" } + if (categories.isNotEmpty()) { + val conditions = categories.mapIndexed { index, cat -> + val paramName = "stockCategory_$index" + args[paramName] = cat + "b.description = :$paramName" + } + "AND (${conditions.joinToString(" OR ")})" + } else { + "" + } + } else { + "" + } + + val sql = """ + SELECT DISTINCT b.code, COALESCE(b.name, '') as name + FROM bom b + WHERE b.deleted = false + AND b.code IS NOT NULL + AND b.code != '' + $stockCategorySql + ORDER BY b.code + """.trimIndent() + + val results = jdbcDao.queryForList(sql, args) + return results.mapNotNull { + val code = it["code"]?.toString() + val name = it["name"]?.toString() ?: "" + if (code != null) { + mapOf("code" to code, "name" to name) + } else { + null + } + } + } + + /** + * Gets list of item codes with their category (FG/WIP) and name based on stockCategory filter. + * Supports multiple categories separated by comma (e.g., "FG,WIP"). + * Returns a list of maps with "code", "category", and "name" keys. + */ + fun getSemiFGItemCodesWithCategory(stockCategory: String?): List> { + val args = mutableMapOf() + + val stockCategorySql = if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) { + // Handle multiple categories (comma-separated) + val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" } + if (categories.isNotEmpty()) { + val conditions = categories.mapIndexed { index, cat -> + val paramName = "stockCategory_$index" + args[paramName] = cat + "b.description = :$paramName" + } + "AND (${conditions.joinToString(" OR ")})" + } else { + "" + } + } else { + "" + } + + val sql = """ + SELECT DISTINCT b.code, COALESCE(b.description, '') as category, COALESCE(b.name, '') as name + FROM bom b + WHERE b.deleted = false + AND b.code IS NOT NULL + AND b.code != '' + $stockCategorySql + ORDER BY b.code + """.trimIndent() + + val results = jdbcDao.queryForList(sql, args) + return results.mapNotNull { + val code = it["code"]?.toString() + val category = it["category"]?.toString() ?: "" + val name = it["name"]?.toString() ?: "" + if (code != null) { + mapOf("code" to code, "category" to category, "name" to name) + } else { + null + } + } + } + /** * Compiles and fills a Jasper Report, returning the PDF as a ByteArray. */ diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt index ddfa12f..ceaf074 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt @@ -85,6 +85,7 @@ class ReportController( @RequestParam(required = false) stockCategory: String?, @RequestParam(required = false) stockSubCategory: String?, @RequestParam(required = false) itemCode: String?, + @RequestParam(required = false) year: String?, @RequestParam(required = false) lastInDateStart: String?, @RequestParam(required = false) lastInDateEnd: String? ): ResponseEntity { @@ -94,6 +95,7 @@ class ReportController( parameters["stockCategory"] = stockCategory ?: "All" parameters["stockSubCategory"] = stockSubCategory ?: "All" parameters["itemNo"] = itemCode ?: "All" + parameters["year"] = year ?: LocalDate.now().year.toString() parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) parameters["lastInDateStart"] = lastInDateStart ?: "" @@ -104,6 +106,7 @@ class ReportController( stockCategory, stockSubCategory, itemCode, + year, lastInDateStart, lastInDateEnd ) @@ -122,4 +125,68 @@ class ReportController( return ResponseEntity(pdfBytes, headers, HttpStatus.OK) } + + @GetMapping("/print-semi-fg-production-analysis") + fun generateSemiFGProductionAnalysisReport( + @RequestParam(required = false) stockCategory: String?, + @RequestParam(required = false) stockSubCategory: String?, + @RequestParam(required = false) itemCode: String?, + @RequestParam(required = false) year: String?, + @RequestParam(required = false) lastOutDateStart: String?, + @RequestParam(required = false) lastOutDateEnd: String? + ): ResponseEntity { + val parameters = mutableMapOf() + + // Set report header parameters + parameters["stockCategory"] = stockCategory ?: "All" + parameters["stockSubCategory"] = stockSubCategory ?: "All" + parameters["itemNo"] = itemCode ?: "All" + parameters["year"] = year ?: LocalDate.now().year.toString() + parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + parameters["lastOutDateStart"] = lastOutDateStart ?: "" + parameters["lastOutDateEnd"] = lastOutDateEnd ?: "" + parameters["deliveryPeriodStart"] = "" + parameters["deliveryPeriodEnd"] = "" + + // Query the DB to get a list of data + val dbData = reportService.searchSemiFGProductionAnalysisReport( + stockCategory, + stockSubCategory, + itemCode, + year, + lastOutDateStart, + lastOutDateEnd + ) + + val pdfBytes = reportService.createPdfResponse( + "/jasper/SemiFGProductionAnalysisReport.jrxml", + parameters, + dbData + ) + + val headers = HttpHeaders().apply { + contentType = MediaType.APPLICATION_PDF + setContentDispositionFormData("attachment", "SemiFGProductionAnalysisReport.pdf") + set("filename", "SemiFGProductionAnalysisReport.pdf") + } + + return ResponseEntity(pdfBytes, headers, HttpStatus.OK) + } + + @GetMapping("/semi-fg-item-codes") + fun getSemiFGItemCodes( + @RequestParam(required = false) stockCategory: String? + ): ResponseEntity>> { + val itemCodes = reportService.getSemiFGItemCodes(stockCategory) + return ResponseEntity(itemCodes, HttpStatus.OK) + } + + @GetMapping("/semi-fg-item-codes-with-category") + fun getSemiFGItemCodesWithCategory( + @RequestParam(required = false) stockCategory: String? + ): ResponseEntity>> { + val itemCodesWithCategory = reportService.getSemiFGItemCodesWithCategory(stockCategory) + return ResponseEntity(itemCodesWithCategory, HttpStatus.OK) + } } \ No newline at end of file diff --git a/src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml b/src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml new file mode 100644 index 0000000..a8a5cc2 --- /dev/null +++ b/src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml @@ -0,0 +1,664 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +