|
|
|
@@ -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) |
|
|
|
} |
|
|
|
} |