Ver a proveniência

adding some charts to test

reset-do-picking-order
Fai Luk há 1 semana
ascendente
cometimento
bff273ea9a
3 ficheiros alterados com 799 adições e 1 eliminações
  1. +610
    -0
      src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt
  2. +187
    -0
      src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt
  3. +2
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt

+ 610
- 0
src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt Ver ficheiro

@@ -0,0 +1,610 @@
package com.ffii.fpsms.modules.chart.service

import com.ffii.core.support.JdbcDao
import org.springframework.stereotype.Service
import java.time.LocalDate

@Service
open class ChartService(
private val jdbcDao: JdbcDao,
) {

/**
* Stock transactions: total qty by date (from stock_ledger).
* Returns list of { date, inQty, outQty, totalQty } for chart.
*/
fun getStockTransactionsByDate(startDate: LocalDate?, endDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(sl.date) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(sl.date) <= :endDate"
} else ""
val sql = """
SELECT
DATE_FORMAT(sl.date, '%Y-%m-%d') AS date,
COALESCE(SUM(sl.inQty), 0) AS inQty,
COALESCE(SUM(sl.outQty), 0) AS outQty,
COALESCE(SUM(COALESCE(sl.inQty, 0) + COALESCE(sl.outQty, 0)), 0) AS totalQty
FROM stock_ledger sl
WHERE sl.deleted = 0 AND sl.date IS NOT NULL
$startSql $endSql
GROUP BY sl.date
ORDER BY sl.date
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Delivery orders: order count and total line qty by date.
* Uses delivery_order.completeDate or estimatedArrivalDate for date.
*/
fun getDeliveryOrderByDate(startDate: LocalDate?, endDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(COALESCE(do.completeDate, do.estimatedArrivalDate, do.orderDate)) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(COALESCE(do.completeDate, do.estimatedArrivalDate, do.orderDate)) <= :endDate"
} else ""
val sql = """
SELECT
DATE_FORMAT(COALESCE(do.completeDate, do.estimatedArrivalDate, do.orderDate), '%Y-%m-%d') AS date,
COUNT(DISTINCT do.id) AS orderCount,
COALESCE(SUM(dol.qty), 0) AS totalQty
FROM delivery_order do
LEFT JOIN delivery_order_line dol ON dol.deliveryOrderId = do.id AND dol.deleted = 0
WHERE do.deleted = 0 $startSql $endSql
GROUP BY DATE(COALESCE(do.completeDate, do.estimatedArrivalDate, do.orderDate))
ORDER BY date
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Purchase orders: count by status (pending, receiving, completed).
* targetDate: when set, only POs whose orderDate is on that date; when null, all POs.
*/
fun getPurchaseOrderByStatus(targetDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val dateSql = if (targetDate != null) {
args["targetDate"] = targetDate.toString()
"AND DATE(po.orderDate) = :targetDate"
} else ""
val sql = """
SELECT
COALESCE(po.status, 'unknown') AS status,
COUNT(po.id) AS count
FROM purchase_order po
WHERE po.deleted = 0
$dateSql
GROUP BY po.status
ORDER BY count DESC
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Stock in vs stock out by date.
* Stock in: stock_in_line.acceptedQty, date from stock_in.completeDate or receiptDate/created.
* Stock out: stock_out_line.qty, date from stock_out.completeDate or created.
*/
fun getStockInOutByDate(startDate: LocalDate?, endDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND u.dt >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND u.dt <= :endDate"
} else ""
val sql = """
SELECT DATE_FORMAT(u.dt, '%Y-%m-%d') AS date,
COALESCE(SUM(u.inQty), 0) AS inQty,
COALESCE(SUM(u.outQty), 0) AS outQty
FROM (
SELECT DATE(COALESCE(si.completeDate, sil.receiptDate, si.created)) AS dt,
SUM(COALESCE(sil.acceptedQty, 0)) AS inQty, 0 AS outQty
FROM stock_in_line sil
INNER JOIN stock_in si ON sil.stockInId = si.id AND si.deleted = 0
WHERE sil.deleted = 0
GROUP BY DATE(COALESCE(si.completeDate, sil.receiptDate, si.created))
UNION ALL
SELECT DATE(COALESCE(so.completeDate, so.created)) AS dt,
0 AS inQty, SUM(COALESCE(sol.qty, 0)) AS outQty
FROM stock_out_line sol
INNER JOIN stock_out so ON sol.stockOutId = so.id AND so.deleted = 0
WHERE sol.deleted = 0
GROUP BY DATE(COALESCE(so.completeDate, so.created))
) u
WHERE 1=1 $startSql $endSql
GROUP BY u.dt
ORDER BY u.dt
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Distinct items that appear in delivery_order_line in the period (for multi-select options).
*/
fun getTopDeliveryItemsItemOptions(startDate: LocalDate?, endDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(COALESCE(do.completeDate, do.estimatedArrivalDate, do.orderDate)) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(COALESCE(do.completeDate, do.estimatedArrivalDate, do.orderDate)) <= :endDate"
} else ""
val sql = """
SELECT DISTINCT it.code AS itemCode, COALESCE(it.name, '') AS itemName
FROM delivery_order_line dol
INNER JOIN delivery_order do ON dol.deliveryOrderId = do.id AND do.deleted = 0
INNER JOIN items it ON dol.itemId = it.id AND it.deleted = 0
WHERE dol.deleted = 0 $startSql $endSql
ORDER BY it.code
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Top delivery items by total qty in the period. When itemCodes is non-empty, only those items (still ordered by totalQty, limit applied).
*/
fun getTopDeliveryItems(
startDate: LocalDate?,
endDate: LocalDate?,
limit: Int,
itemCodes: List<String>?
): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>("limit" to limit)
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(COALESCE(do.completeDate, do.estimatedArrivalDate, do.orderDate)) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(COALESCE(do.completeDate, do.estimatedArrivalDate, do.orderDate)) <= :endDate"
} else ""
val itemSql = if (!itemCodes.isNullOrEmpty()) {
val codes = itemCodes.map { it.trim() }.filter { it.isNotBlank() }
if (codes.isEmpty()) "" else {
args["itemCodes"] = codes
"AND it.code IN (:itemCodes)"
}
} else ""
val sql = """
SELECT
it.code AS itemCode,
it.name AS itemName,
SUM(COALESCE(dol.qty, 0)) AS totalQty
FROM delivery_order_line dol
INNER JOIN delivery_order do ON dol.deliveryOrderId = do.id AND do.deleted = 0
INNER JOIN items it ON dol.itemId = it.id AND it.deleted = 0
WHERE dol.deleted = 0 $startSql $endSql $itemSql
GROUP BY dol.itemId, it.code, it.name
ORDER BY totalQty DESC
LIMIT :limit
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Stock balance trend: one series per item (or aggregated) by date from stock_ledger.
* If itemCode is provided, filter to that item; otherwise returns daily total balance sum (across items).
*/
fun getStockBalanceTrend(
startDate: LocalDate?,
endDate: LocalDate?,
itemCode: String?
): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND sl.date >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND sl.date <= :endDate"
} else ""
val itemSql = if (!itemCode.isNullOrBlank()) {
args["itemCode"] = "%$itemCode%"
"AND sl.itemCode LIKE :itemCode"
} else ""
val sql = """
SELECT
DATE_FORMAT(sl.date, '%Y-%m-%d') AS date,
COALESCE(SUM(sl.balance), 0) AS balance
FROM stock_ledger sl
WHERE sl.deleted = 0 AND sl.date IS NOT NULL $startSql $endSql $itemSql
GROUP BY sl.date
ORDER BY sl.date
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Consumption trend (out qty by month) for chart - from stock_ledger outQty.
* Optional itemCode filter.
*/
fun getConsumptionTrendByMonth(
year: Int?,
startDate: LocalDate?,
endDate: LocalDate?,
itemCode: String?
): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val yearSql = if (year != null) {
args["year"] = year
"AND YEAR(sl.date) = :year"
} else ""
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND sl.date >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND sl.date <= :endDate"
} else ""
val itemSql = if (!itemCode.isNullOrBlank()) {
args["itemCode"] = "%$itemCode%"
"AND sl.itemCode LIKE :itemCode"
} else ""
val sql = """
SELECT
DATE_FORMAT(sl.date, '%Y-%m') AS month,
COALESCE(SUM(sl.outQty), 0) AS outQty
FROM stock_ledger sl
WHERE sl.deleted = 0 AND sl.date IS NOT NULL $yearSql $startSql $endSql $itemSql
GROUP BY DATE_FORMAT(sl.date, '%Y-%m')
ORDER BY month
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Returns staff from user table for multi-select filter: staffNo and name, only users that have staffNo.
*/
fun getStaffDeliveryPerformanceHandlers(): List<Map<String, Any>> {
val sql = """
SELECT u.staffNo AS staffNo, COALESCE(u.name, '') AS name
FROM user u
WHERE u.deleted = 0
AND u.staffNo IS NOT NULL
AND TRIM(u.staffNo) <> ''
ORDER BY u.staffNo
""".trimIndent()
return jdbcDao.queryForList(sql, emptyMap<String, Any>())
}

/**
* Staff delivery performance: daily pick ticket count and total time per staff.
* Uses do_pick_order_record (handler = handledBy); time = sum of (ticketCompleteDateTime - ticketReleaseTime) per record.
* Optionally use do_pick_order_line_record for line count; here orderCount = number of completed pick tickets.
* staffNos: when non-empty, filter to these staff by user.staffNo (multi-select).
*/
fun getStaffDeliveryPerformance(
startDate: LocalDate?,
endDate: LocalDate?,
staffNos: List<String>?
): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(dpor.ticketCompleteDateTime) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(dpor.ticketCompleteDateTime) <= :endDate"
} else ""
val staffSql = if (!staffNos.isNullOrEmpty()) {
val nos = staffNos.map { it.trim() }.filter { it.isNotBlank() }
if (nos.isEmpty()) "" else {
args["staffNos"] = nos
"AND u.staffNo IN (:staffNos)"
}
} else ""
val sql = """
SELECT
DATE_FORMAT(dpor.ticketCompleteDateTime, '%Y-%m-%d') AS date,
COALESCE(u.name, dpor.handler_name, 'Unknown') AS staffName,
COUNT(dpor.id) AS orderCount,
COALESCE(SUM(
CASE
WHEN dpor.ticketReleaseTime IS NOT NULL AND dpor.ticketCompleteDateTime IS NOT NULL
THEN GREATEST(0, TIMESTAMPDIFF(MINUTE, dpor.ticket_release_time, dpor.ticketCompleteDateTime))
ELSE 0
END
), 0) AS totalMinutes
FROM do_pick_order_record dpor
LEFT JOIN user u ON dpor.handled_by = u.id AND u.deleted = 0
WHERE dpor.deleted = 0
AND dpor.ticket_status = 'completed'
AND dpor.ticketCompleteDateTime IS NOT NULL
$startSql $endSql $staffSql
GROUP BY DATE(dpor.ticketCompleteDateTime), dpor.handled_by, u.name, dpor.handler_name
ORDER BY date, orderCount DESC
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

// ---------- Job order reports ----------

/**
* Job order count by status. targetDate: when set, only JOs with planStart on that date.
*/
fun getJobOrderByStatus(targetDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val dateSql = if (targetDate != null) {
args["targetDate"] = targetDate.toString()
"AND DATE(jo.planStart) = :targetDate"
} else ""
val sql = """
SELECT COALESCE(jo.status, 'unknown') AS status, COUNT(jo.id) AS count
FROM job_order jo
WHERE jo.deleted = 0
$dateSql
GROUP BY jo.status
ORDER BY count DESC
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Job order count by date (by planStart date).
*/
fun getJobOrderCountByDate(startDate: LocalDate?, endDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(jo.planStart) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(jo.planStart) <= :endDate"
} else ""
val sql = """
SELECT DATE_FORMAT(jo.planStart, '%Y-%m-%d') AS date, COUNT(jo.id) AS orderCount
FROM job_order jo
WHERE jo.deleted = 0 AND jo.planStart IS NOT NULL
$startSql $endSql
GROUP BY DATE(jo.planStart)
ORDER BY date
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Job order created vs completed by date: created count (by created date), completed count (by actualEnd date).
* Uses two simple queries and merges by date to avoid SQL complexity and parameter reuse issues.
*/
fun getJobOrderCreatedCompletedByDate(startDate: LocalDate?, endDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(jo.created) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(jo.created) <= :endDate"
} else ""
val createdSql = """
SELECT DATE_FORMAT(jo.created, '%Y-%m-%d') AS date, COUNT(jo.id) AS createdCount
FROM job_order jo
WHERE jo.deleted = 0 AND jo.created IS NOT NULL $startSql $endSql
GROUP BY DATE(jo.created)
ORDER BY date
""".trimIndent()
val createdRows = jdbcDao.queryForList(createdSql, args)

val args2 = mutableMapOf<String, Any>()
val startSql2 = if (startDate != null) {
args2["startDate"] = startDate.toString()
"AND DATE(jo.actualEnd) >= :startDate"
} else ""
val endSql2 = if (endDate != null) {
args2["endDate"] = endDate.toString()
"AND DATE(jo.actualEnd) <= :endDate"
} else ""
val completedSql = """
SELECT DATE_FORMAT(jo.actualEnd, '%Y-%m-%d') AS date, COUNT(jo.id) AS completedCount
FROM job_order jo
WHERE jo.deleted = 0 AND jo.actualEnd IS NOT NULL AND jo.status = 'completed' $startSql2 $endSql2
GROUP BY DATE(jo.actualEnd)
ORDER BY date
""".trimIndent()
val completedRows = jdbcDao.queryForList(completedSql, args2)

val dateToCreated = createdRows.mapNotNull { r ->
val d = r["date"]?.toString()
val c = (r["createdCount"] as? Number)?.toInt() ?: 0
if (d != null) d to c else null
}.toMap()
val dateToCompleted = completedRows.mapNotNull { r ->
val d = r["date"]?.toString()
val c = (r["completedCount"] as? Number)?.toInt() ?: 0
if (d != null) d to c else null
}.toMap()
val allDates = (dateToCreated.keys + dateToCompleted.keys).sorted()
return allDates.map { dt ->
mapOf(
"date" to dt,
"createdCount" to (dateToCreated[dt] ?: 0),
"completedCount" to (dateToCompleted[dt] ?: 0)
)
}
}

/**
* Job order material: pending vs picked (by planStart date).
* From job_order_bom_material: status pending = 待領, completed = 已揀.
*/
fun getJobMaterialPendingPickedByDate(startDate: LocalDate?, endDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(jo.planStart) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(jo.planStart) <= :endDate"
} else ""
val sql = """
SELECT DATE_FORMAT(jo.planStart, '%Y-%m-%d') AS date,
COALESCE(SUM(CASE WHEN jobm.status = 'pending' THEN 1 ELSE 0 END), 0) AS pendingCount,
COALESCE(SUM(CASE WHEN jobm.status = 'completed' THEN 1 ELSE 0 END), 0) AS pickedCount
FROM job_order jo
LEFT JOIN job_order_bom_material jobm ON jobm.jobOrderId = jo.id AND jobm.deleted = 0
WHERE jo.deleted = 0 AND jo.planStart IS NOT NULL
$startSql $endSql
GROUP BY DATE(jo.planStart)
ORDER BY date
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Job order process: pending vs completed by date (by jo planStart).
* Pending = endTime IS NULL, completed = endTime IS NOT NULL.
*/
fun getJobProcessPendingCompletedByDate(startDate: LocalDate?, endDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(jo.planStart) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(jo.planStart) <= :endDate"
} else ""
val sql = """
SELECT DATE_FORMAT(jo.planStart, '%Y-%m-%d') AS date,
COALESCE(SUM(CASE WHEN jop.endTime IS NULL THEN 1 ELSE 0 END), 0) AS pendingCount,
COALESCE(SUM(CASE WHEN jop.endTime IS NOT NULL THEN 1 ELSE 0 END), 0) AS completedCount
FROM job_order jo
JOIN job_order_process jop ON jop.joId = jo.id AND jop.deleted = 0
WHERE jo.deleted = 0 AND jo.planStart IS NOT NULL
$startSql $endSql
GROUP BY DATE(jo.planStart)
ORDER BY date
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Job order equipment: working vs worked by date (by process detail operatingStart).
* Working = operatingEnd IS NULL, worked = operatingEnd IS NOT NULL.
*/
fun getJobEquipmentWorkingWorkedByDate(startDate: LocalDate?, endDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(COALESCE(jopd.operatingStart, jo.planStart)) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(COALESCE(jopd.operatingStart, jo.planStart)) <= :endDate"
} else ""
val sql = """
SELECT DATE_FORMAT(COALESCE(jopd.operatingStart, jo.planStart), '%Y-%m-%d') AS date,
COALESCE(SUM(CASE WHEN jopd.operatingEnd IS NULL THEN 1 ELSE 0 END), 0) AS workingCount,
COALESCE(SUM(CASE WHEN jopd.operatingEnd IS NOT NULL THEN 1 ELSE 0 END), 0) AS workedCount
FROM job_order_process_detail jopd
JOIN job_order_process jop ON jop.id = jopd.jopId AND jop.deleted = 0
JOIN job_order jo ON jo.id = jop.joId AND jo.deleted = 0
WHERE jopd.deleted = 0 AND (jopd.operatingStart IS NOT NULL OR jo.planStart IS NOT NULL)
$startSql $endSql
GROUP BY DATE(COALESCE(jopd.operatingStart, jo.planStart))
ORDER BY date
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

// ---------- Forecast & planning reports ----------

/**
* Production schedule by produce date: scheduled item count (已排物料) and total estimated prod count.
*/
fun getProductionScheduleByDate(startDate: LocalDate?, endDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(ps.produceAt) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(ps.produceAt) <= :endDate"
} else ""
val startSql2 = if (startDate != null) "AND DATE(ps2.produceAt) >= :startDate" else ""
val endSql2 = if (endDate != null) "AND DATE(ps2.produceAt) <= :endDate" else ""
val sql = """
SELECT DATE_FORMAT(ps.produceAt, '%Y-%m-%d') AS date,
COUNT(DISTINCT psl.itemId) AS scheduledItemCount,
(SELECT COALESCE(SUM(ps2.totalEstProdCount), 0)
FROM production_schedule ps2
WHERE DATE(ps2.produceAt) = DATE(ps.produceAt) AND ps2.deleted = 0
$startSql2 $endSql2) AS totalEstProdCount
FROM production_schedule ps
LEFT JOIN production_schedule_line psl ON psl.prodScheduleId = ps.id AND psl.deleted = 0
WHERE ps.deleted = 0 AND ps.produceAt IS NOT NULL
$startSql $endSql
GROUP BY DATE(ps.produceAt)
ORDER BY date
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Planned daily output (forecast): from item_daily_out + items, for chart (top items by daily qty).
* Optional limit. Returns itemCode, itemName, dailyQty.
*/
fun getPlannedDailyOutputByItem(limit: Int): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>("limit" to limit.coerceIn(1, 200))
val sql = """
SELECT ido.itemCode AS itemCode, COALESCE(it.name, '') AS itemName, COALESCE(ido.dailyQty, 0) AS dailyQty
FROM item_daily_out ido
LEFT JOIN items it ON it.code = ido.itemCode AND it.deleted = 0
WHERE ido.dailyQty IS NOT NULL AND ido.dailyQty > 0
ORDER BY ido.dailyQty DESC
LIMIT :limit
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Planned production by date and by item (from production_schedule + production_schedule_line).
* Returns [{ date, itemCode, itemName, qty }] for chart: all items' planned production per date.
*/
fun getPlannedOutputByDateAndItem(startDate: LocalDate?, endDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val startSql = if (startDate != null) {
args["startDate"] = startDate.toString()
"AND DATE(ps.produceAt) >= :startDate"
} else ""
val endSql = if (endDate != null) {
args["endDate"] = endDate.toString()
"AND DATE(ps.produceAt) <= :endDate"
} else ""
val sql = """
SELECT
DATE_FORMAT(ps.produceAt, '%Y-%m-%d') AS date,
it.code AS itemCode,
COALESCE(it.name, '') AS itemName,
COALESCE(SUM(psl.prodQty), 0) AS qty
FROM production_schedule ps
JOIN production_schedule_line psl ON psl.prodScheduleId = ps.id AND psl.deleted = 0
JOIN items it ON it.id = psl.itemId AND it.deleted = 0
WHERE ps.deleted = 0 AND ps.produceAt IS NOT NULL
$startSql $endSql
GROUP BY DATE(ps.produceAt), it.code, it.name
ORDER BY date, itemCode
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}
}

+ 187
- 0
src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt Ver ficheiro

@@ -0,0 +1,187 @@
package com.ffii.fpsms.modules.chart.web

import com.ffii.fpsms.modules.chart.service.ChartService
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.time.LocalDate

@RestController
@RequestMapping("/chart")
class ChartController(
private val chartService: ChartService,
) {

/**
* GET /chart/stock-transactions-by-date?startDate=2025-01-01&endDate=2025-01-31
* Returns [{ date, inQty, outQty, totalQty }]
*/
@GetMapping("/stock-transactions-by-date")
fun getStockTransactionsByDate(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
): List<Map<String, Any>> = chartService.getStockTransactionsByDate(startDate, endDate)

/**
* GET /chart/delivery-order-by-date?startDate=&endDate=
* Returns [{ date, orderCount, totalQty }]
*/
@GetMapping("/delivery-order-by-date")
fun getDeliveryOrderByDate(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
): List<Map<String, Any>> = chartService.getDeliveryOrderByDate(startDate, endDate)

/**
* GET /chart/purchase-order-by-status?targetDate=2025-03-15
* Returns [{ status, count }]. targetDate defaults to today on frontend; when set, only POs with orderDate on that date.
*/
@GetMapping("/purchase-order-by-status")
fun getPurchaseOrderByStatus(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) targetDate: LocalDate?,
): List<Map<String, Any>> =
chartService.getPurchaseOrderByStatus(targetDate)

/**
* GET /chart/stock-in-out-by-date?startDate=&endDate=
* Returns [{ date, inQty, outQty }]
*/
@GetMapping("/stock-in-out-by-date")
fun getStockInOutByDate(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
): List<Map<String, Any>> = chartService.getStockInOutByDate(startDate, endDate)

/**
* GET /chart/top-delivery-items-item-options?startDate=&endDate=
* Returns [{ itemCode, itemName }] — distinct items in delivery lines in the period (for multi-select).
*/
@GetMapping("/top-delivery-items-item-options")
fun getTopDeliveryItemsItemOptions(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
): List<Map<String, Any>> = chartService.getTopDeliveryItemsItemOptions(startDate, endDate)

/**
* GET /chart/top-delivery-items?startDate=&endDate=&limit=20&itemCode=A&itemCode=B
* Returns [{ itemCode, itemName, totalQty }]. When itemCode present, only those items (still by totalQty, limit).
*/
@GetMapping("/top-delivery-items")
fun getTopDeliveryItems(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
@RequestParam(required = false, defaultValue = "10") limit: Int,
@RequestParam(required = false) itemCode: List<String>?,
): List<Map<String, Any>> =
chartService.getTopDeliveryItems(startDate, endDate, limit.coerceIn(1, 200), itemCode)

/**
* GET /chart/stock-balance-trend?startDate=&endDate=&itemCode=
* Returns [{ date, balance }]
*/
@GetMapping("/stock-balance-trend")
fun getStockBalanceTrend(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
@RequestParam(required = false) itemCode: String?,
): List<Map<String, Any>> = chartService.getStockBalanceTrend(startDate, endDate, itemCode)

/**
* GET /chart/consumption-trend-by-month?year=&startDate=&endDate=&itemCode=
* Returns [{ month, outQty }]
*/
@GetMapping("/consumption-trend-by-month")
fun getConsumptionTrendByMonth(
@RequestParam(required = false) year: Int?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
@RequestParam(required = false) itemCode: String?,
): List<Map<String, Any>> =
chartService.getConsumptionTrendByMonth(year, startDate, endDate, itemCode)

/**
* GET /chart/staff-delivery-performance-handlers — staff from user table (staffNo, name), only those with staffNo.
*/
@GetMapping("/staff-delivery-performance-handlers")
fun getStaffDeliveryPerformanceHandlers(): List<Map<String, Any>> =
chartService.getStaffDeliveryPerformanceHandlers()

/**
* GET /chart/staff-delivery-performance?startDate=&endDate=&staffNo=A001&staffNo=A002
* Returns [{ date, staffName, orderCount, totalMinutes }]. Data from do_pick_order_record (handled_by), orderCount = completed pick tickets, totalMinutes = sum(ticketCompleteDateTime - ticketReleaseTime).
*/
@GetMapping("/staff-delivery-performance")
fun getStaffDeliveryPerformance(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
@RequestParam(required = false) staffNo: List<String>?,
): List<Map<String, Any>> =
chartService.getStaffDeliveryPerformance(startDate, endDate, staffNo)

// ---------- Job order reports ----------

/** GET /chart/job-order-by-status?targetDate= — count by status, optional planStart date filter. */
@GetMapping("/job-order-by-status")
fun getJobOrderByStatus(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) targetDate: LocalDate?,
): List<Map<String, Any>> = chartService.getJobOrderByStatus(targetDate)

/** GET /chart/job-order-count-by-date?startDate=&endDate= — count by planStart date. */
@GetMapping("/job-order-count-by-date")
fun getJobOrderCountByDate(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
): List<Map<String, Any>> = chartService.getJobOrderCountByDate(startDate, endDate)

/** GET /chart/job-order-created-completed-by-date?startDate=&endDate= — created vs completed by date. */
@GetMapping("/job-order-created-completed-by-date")
fun getJobOrderCreatedCompletedByDate(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
): List<Map<String, Any>> = chartService.getJobOrderCreatedCompletedByDate(startDate, endDate)

/** GET /chart/job-material-pending-picked-by-date?startDate=&endDate= — material 待領/已揀 by date. */
@GetMapping("/job-material-pending-picked-by-date")
fun getJobMaterialPendingPickedByDate(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
): List<Map<String, Any>> = chartService.getJobMaterialPendingPickedByDate(startDate, endDate)

/** GET /chart/job-process-pending-completed-by-date?startDate=&endDate= — process 待完成/已完成 by date. */
@GetMapping("/job-process-pending-completed-by-date")
fun getJobProcessPendingCompletedByDate(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
): List<Map<String, Any>> = chartService.getJobProcessPendingCompletedByDate(startDate, endDate)

/** GET /chart/job-equipment-working-worked-by-date?startDate=&endDate= — equipment 使用中/已使用 by date. */
@GetMapping("/job-equipment-working-worked-by-date")
fun getJobEquipmentWorkingWorkedByDate(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
): List<Map<String, Any>> = chartService.getJobEquipmentWorkingWorkedByDate(startDate, endDate)

// ---------- Forecast & planning ----------

/** GET /chart/production-schedule-by-date?startDate=&endDate= — schedule count & total est prod by produce date. */
@GetMapping("/production-schedule-by-date")
fun getProductionScheduleByDate(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
): List<Map<String, Any>> = chartService.getProductionScheduleByDate(startDate, endDate)

/** GET /chart/planned-daily-output-by-item?limit=20 — top items by planned daily qty (item_daily_out). */
@GetMapping("/planned-daily-output-by-item")
fun getPlannedDailyOutputByItem(
@RequestParam(required = false, defaultValue = "20") limit: Int,
): List<Map<String, Any>> = chartService.getPlannedDailyOutputByItem(limit.coerceIn(1, 200))

/** GET /chart/planned-output-by-date-and-item?startDate=&endDate= — all items' planned production by date (production_schedule). */
@GetMapping("/planned-output-by-date-and-item")
fun getPlannedOutputByDateAndItem(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) startDate: LocalDate?,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
): List<Map<String, Any>> = chartService.getPlannedOutputByDateAndItem(startDate, endDate)
}

+ 2
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt Ver ficheiro

@@ -194,7 +194,8 @@ open class PlasticBagPrinterService(
val stockInLineId = stockInLine.id ?: return@forEach

val qrContent = """{"itemId": $itemId, "stockInLineId": $stockInLineId}"""
val bmp = createQrCodeBitmap(qrContent, 600)
// Trim 90% of top/bottom/side whitespace: keep 4px padding per side (was 40) → totalSize = contentSize + 8
val bmp = createQrCodeBitmap(qrContent, 600, 600 + 8)
val zipEntryName = buildUniqueZipEntryName(filename, addedEntries)
if (!addedEntries.add(zipEntryName)) return@forEach
addToZip(zos, zipEntryName, bmp.bytes)


Carregando…
Cancelar
Guardar