From 95ddc8d0779d0503831e4f7d9144ecf17e86007b Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Sat, 7 Feb 2026 19:58:07 +0800 Subject: [PATCH] Stock Item Consumption Trend Report --- .../SemiFGProductionAnalysisReportService.kt | 256 +++++++ .../StockItemConsumptionTrendReportService.kt | 226 ++++++ ...emiFGProductionAnalysisReportController.kt | 82 +++ ...ockItemConsumptionTrendReportController.kt | 79 +++ .../StockItemConsumptionTrendReport.jrxml | 650 ++++++++++++++++++ 5 files changed, 1293 insertions(+) create mode 100644 src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/report/service/StockItemConsumptionTrendReportService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/report/web/StockItemConsumptionTrendReportController.kt create mode 100644 src/main/resources/jasper/StockItemConsumptionTrendReport.jrxml diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt new file mode 100644 index 0000000..3053ca5 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt @@ -0,0 +1,256 @@ +package com.ffii.fpsms.modules.report.service + +import org.springframework.stereotype.Service +import com.ffii.core.support.JdbcDao + +@Service +class SemiFGProductionAnalysisReportService( + private val jdbcDao: JdbcDao, +) { + /** + * 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 { + 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 { + 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 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 + } + } + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/StockItemConsumptionTrendReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/StockItemConsumptionTrendReportService.kt new file mode 100644 index 0000000..42a6354 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/report/service/StockItemConsumptionTrendReportService.kt @@ -0,0 +1,226 @@ +package com.ffii.fpsms.modules.report.service + +import org.springframework.stereotype.Service +import com.ffii.core.support.JdbcDao + +@Service +class StockItemConsumptionTrendReportService( + private val jdbcDao: JdbcDao, +) { + /** + * 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 { + 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 { + 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 Item Consumption Trend Report data. + * Flow: + * 1. Filter items by type (MAT, WIP) - this is the stockCategory filter + * 2. Use items.code to match stock_ledger.itemCode + * 3. Aggregate stock_ledger.outQty by month based on stock_ledger.modified + * Supports comma-separated values for stockCategory, stockSubCategory, and itemCode. + */ + fun searchStockItemConsumptionTrendReport( + stockCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String? + ): List> { + val args = mutableMapOf() + + // Filter by stockCategory from items.type (MAT/WIP/NM/FG/CMB) + // Supports multiple categories separated by comma (e.g., "MAT,WIP,NM") + // Valid categories: MAT, WIP, NM, FG, CMB + // If "All" is selected or empty, show all valid categories + // If specific categories are selected, show only those + val validCategories = setOf("MAT", "WIP", "NM", "FG", "CMB") + 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" && validCategories.contains(it) } + if (categories.isNotEmpty()) { + val conditions = categories.mapIndexed { index, cat -> + val paramName = "stockCategory_$index" + args[paramName] = cat + "it.type = :$paramName" + } + "AND (${conditions.joinToString(" OR ")})" + } else { + // If no valid categories, default to all valid categories + "AND (it.type = 'MAT' OR it.type = 'WIP' OR it.type = 'NM' OR it.type = 'FG' OR it.type = 'CMB')" + } + } else { + // If "All" or empty, filter to all valid categories + "AND (it.type = 'MAT' OR it.type = 'WIP' OR it.type = 'NM' OR it.type = 'FG' OR it.type = 'CMB')" + } + + // itemCode now contains prefixes (first 2 letters), so we use LIKE to match codes starting with those prefixes + val itemCodeSql = if (!itemCode.isNullOrBlank()) { + val prefixes = itemCode.split(",").map { it.trim() }.filter { it.isNotBlank() } + if (prefixes.isNotEmpty()) { + val conditions = prefixes.mapIndexed { index, prefix -> + val paramName = "itemCode_$index" + args[paramName] = "$prefix%" + "it.code LIKE :$paramName" + } + "AND (${conditions.joinToString(" OR ")})" + } else { + "" + } + } else { + "" + } + + 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(it.code, '') as itemNo, + COALESCE(it.name, '') as itemName, + COALESCE(uc.code, '') as unitOfMeasure, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 1 THEN COALESCE(sl.outQty, 0) ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJan, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 2 THEN COALESCE(sl.outQty, 0) ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyFeb, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 3 THEN COALESCE(sl.outQty, 0) ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyMar, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 4 THEN COALESCE(sl.outQty, 0) ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyApr, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 5 THEN COALESCE(sl.outQty, 0) ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyMay, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 6 THEN COALESCE(sl.outQty, 0) ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJun, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 7 THEN COALESCE(sl.outQty, 0) ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJul, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 8 THEN COALESCE(sl.outQty, 0) ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyAug, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 9 THEN COALESCE(sl.outQty, 0) ELSE 0 END), 0) AS DECIMAL(18,2)) as qtySep, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 10 THEN COALESCE(sl.outQty, 0) ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyOct, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 11 THEN COALESCE(sl.outQty, 0) ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyNov, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 12 THEN COALESCE(sl.outQty, 0) ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyDec, + CAST(COALESCE(SUM(COALESCE(sl.outQty, 0)), 0) AS CHAR) as totalProductionQty + FROM items it + INNER JOIN stock_ledger sl ON it.code = sl.itemCode + 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 it.deleted = false + AND sl.deleted = false + AND sl.itemCode IS NOT NULL + AND sl.itemCode != '' + AND COALESCE(sl.outQty, 0) > 0 + $stockCategorySql + $itemCodeSql + $yearSql + $lastOutDateStartSql + $lastOutDateEndSql + GROUP BY it.id, ic.sub, it.code, it.name, it.description, uc.code + ORDER BY ic.sub, it.code + """.trimIndent() + + return jdbcDao.queryForList(sql, args) + } + + /** + * Gets list of item code prefixes (first 2 letters) based on stockCategory filter. + * Supports multiple categories separated by comma (e.g., "MAT,WIP,NM"). + * If stockCategory is "All" or null, returns prefixes for all valid categories (MAT, WIP, NM, FG, CMB). + * Returns a list of maps with "prefix" key. + */ + fun getStockItemCodePrefixes(stockCategory: String?): List> { + val args = mutableMapOf() + val validCategories = setOf("MAT", "WIP", "NM", "FG", "CMB") + + 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" && validCategories.contains(it) } + if (categories.isNotEmpty()) { + val conditions = categories.mapIndexed { index, cat -> + val paramName = "stockCategory_$index" + args[paramName] = cat + "it.type = :$paramName" + } + "AND (${conditions.joinToString(" OR ")})" + } else { + "AND (it.type = 'MAT' OR it.type = 'WIP' OR it.type = 'NM' OR it.type = 'FG' OR it.type = 'CMB')" + } + } else { + "AND (it.type = 'MAT' OR it.type = 'WIP' OR it.type = 'NM' OR it.type = 'FG' OR it.type = 'CMB')" + } + + val sql = """ + SELECT DISTINCT LEFT(it.code, 2) as prefix + FROM items it + INNER JOIN stock_ledger sl ON it.code = sl.itemCode + WHERE it.deleted = false + AND sl.deleted = false + AND sl.itemCode IS NOT NULL + AND sl.itemCode != '' + AND COALESCE(sl.outQty, 0) > 0 + AND LENGTH(it.code) >= 2 + $stockCategorySql + ORDER BY prefix + """.trimIndent() + + val results = jdbcDao.queryForList(sql, args) + return results.mapNotNull { + val prefix = it["prefix"]?.toString() + if (prefix != null && prefix.isNotBlank()) { + mapOf("prefix" to prefix, "label" to prefix, "value" to prefix) + } else { + null + } + } + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt new file mode 100644 index 0000000..9832d31 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt @@ -0,0 +1,82 @@ +package com.ffii.fpsms.modules.report.web + +import net.sf.jasperreports.engine.* +import org.springframework.http.* +import org.springframework.web.bind.annotation.* +import java.time.LocalDate +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import com.ffii.fpsms.modules.report.service.SemiFGProductionAnalysisReportService +import com.ffii.fpsms.modules.report.service.ReportService + +@RestController +@RequestMapping("/report") +class SemiFGProductionAnalysisReportController( + private val semiFGProductionAnalysisReportService: SemiFGProductionAnalysisReportService, + private val reportService: ReportService, +) { + + @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 = semiFGProductionAnalysisReportService.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 = semiFGProductionAnalysisReportService.getSemiFGItemCodes(stockCategory) + return ResponseEntity(itemCodes, HttpStatus.OK) + } + + @GetMapping("/semi-fg-item-codes-with-category") + fun getSemiFGItemCodesWithCategory( + @RequestParam(required = false) stockCategory: String? + ): ResponseEntity>> { + val itemCodesWithCategory = semiFGProductionAnalysisReportService.getSemiFGItemCodesWithCategory(stockCategory) + return ResponseEntity(itemCodesWithCategory, HttpStatus.OK) + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/StockItemConsumptionTrendReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/StockItemConsumptionTrendReportController.kt new file mode 100644 index 0000000..d3bf504 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/report/web/StockItemConsumptionTrendReportController.kt @@ -0,0 +1,79 @@ +package com.ffii.fpsms.modules.report.web + +import net.sf.jasperreports.engine.* +import org.springframework.http.* +import org.springframework.web.bind.annotation.* +import java.time.LocalDate +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import com.ffii.fpsms.modules.report.service.StockItemConsumptionTrendReportService +import com.ffii.fpsms.modules.report.service.ReportService + +@RestController +@RequestMapping("/report") +class StockItemConsumptionTrendReportController( + private val stockItemConsumptionTrendReportService: StockItemConsumptionTrendReportService, + private val reportService: ReportService, +) { + + @GetMapping("/print-stock-item-consumption-trend") + fun generateStockItemConsumptionTrendReport( + @RequestParam(required = false) stockCategory: String?, + @RequestParam(required = false) itemCode: String?, + @RequestParam(required = false) year: String?, + @RequestParam(required = false) lastOutDateStart: String?, + @RequestParam(required = false) lastOutDateEnd: String? + ): ResponseEntity { + try { + val parameters = mutableMapOf() + + // Set report header parameters + parameters["stockCategory"] = stockCategory ?: "All" + parameters["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 = stockItemConsumptionTrendReportService.searchStockItemConsumptionTrendReport( + stockCategory, + itemCode, + year, + lastOutDateStart, + lastOutDateEnd + ) + + val pdfBytes = reportService.createPdfResponse( + "/jasper/StockItemConsumptionTrendReport.jrxml", + parameters, + dbData + ) + + val headers = HttpHeaders().apply { + contentType = MediaType.APPLICATION_PDF + setContentDispositionFormData("attachment", "StockItemConsumptionTrendReport.pdf") + set("filename", "StockItemConsumptionTrendReport.pdf") + } + + return ResponseEntity(pdfBytes, headers, HttpStatus.OK) + } catch (e: Exception) { + e.printStackTrace() + val errorMessage = e.message ?: "Unknown error occurred" + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(errorMessage.toByteArray()) + } + } + + @GetMapping("/stock-item-code-prefixes") + fun getStockItemCodePrefixes( + @RequestParam(required = false) stockCategory: String? + ): ResponseEntity>> { + val prefixes = stockItemConsumptionTrendReportService.getStockItemCodePrefixes(stockCategory) + return ResponseEntity(prefixes, HttpStatus.OK) + } +} diff --git a/src/main/resources/jasper/StockItemConsumptionTrendReport.jrxml b/src/main/resources/jasper/StockItemConsumptionTrendReport.jrxml new file mode 100644 index 0000000..92b7c6e --- /dev/null +++ b/src/main/resources/jasper/StockItemConsumptionTrendReport.jrxml @@ -0,0 +1,650 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +