Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

master
CANCERYS\kw093 7 часов назад
Родитель
Сommit
8b8f33a199
2 измененных файлов: 560 добавлений и 0 удалений
  1. +529
    -0
      src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt
  2. +31
    -0
      src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt

+ 529
- 0
src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt Просмотреть файл

@@ -962,6 +962,535 @@ open class ChartService(
return jdbcDao.queryForList(sql, args)
}

/**
* Equipment usage board: equipment usage for the selected calendar day.
*
* **Legacy:** one row per [job_order_process_detail] when the parent step is closed
* ([job_order_process.endTime] or job **completed**) and the row matches the day.
*
* **Production flow (工藝流程):** one row per [productprocessline] with equipment (id, detail, or free-text
* [equipment_name]) when the line is done (Pass/Completed / [endTime]) or the step/job is closed—aligned with
* `/jo/edit` line display. [jopdId] is **negative** for line-sourced rows (stable client key; not a real jopd id).
*
* Day filter: `COALESCE(operatingEnd, operatingStart, …)` for jopd; for lines
* `COALESCE(line.endTime, line.startTime, jop.endTime, planStart)`.
* Each row includes **usageMinutes** (jopd: operating span; line: start–end minutes or [processingTime] when Pass).
* - [targetDate] null: uses server **LocalDate.now** (prefer passing the client calendar date from the UI).
*/
fun getEquipmentUsageBoardRows(targetDate: LocalDate?): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val effectiveDate = targetDate ?: LocalDate.now()
args["eqUsageDate"] = effectiveDate.toString()
val sql = """
SELECT
t.jopdId AS jopdId,
t.equipmentId AS equipmentId,
t.equipmentCode AS equipmentCode,
t.equipmentName AS equipmentName,
t.jobOrderId AS jobOrderId,
t.jobOrderCode AS jobOrderCode,
t.jobPlanStart AS jobPlanStart,
t.processCode AS processCode,
t.processName AS processName,
t.operatingStart AS operatingStart,
t.operatingEnd AS operatingEnd,
t.usageMinutes AS usageMinutes,
t.workingNow AS workingNow,
t.operatorUsername AS operatorUsername,
t.operatorName AS operatorName
FROM (
SELECT
jopd.id AS jopdId,
e.id AS equipmentId,
e.code AS equipmentCode,
e.name AS equipmentName,
jo.id AS jobOrderId,
jo.code AS jobOrderCode,
DATE_FORMAT(jo.planStart, '%Y-%m-%d %H:%i:%s') AS jobPlanStart,
p.code AS processCode,
p.name AS processName,
DATE_FORMAT(jopd.operatingStart, '%Y-%m-%d %H:%i:%s') AS operatingStart,
DATE_FORMAT(jopd.operatingEnd, '%Y-%m-%d %H:%i:%s') AS operatingEnd,
CASE
WHEN jopd.operatingStart IS NOT NULL AND jopd.operatingEnd IS NOT NULL
THEN GREATEST(0, TIMESTAMPDIFF(MINUTE, jopd.operatingStart, jopd.operatingEnd))
ELSE 0
END AS usageMinutes,
CASE
WHEN jopd.operatingStart IS NOT NULL AND jopd.operatingEnd IS NULL THEN 1
ELSE 0
END AS workingNow,
COALESCE(u.username, '') AS operatorUsername,
COALESCE(NULLIF(TRIM(COALESCE(u.fullname, '')), ''), u.name, u.username, '') AS operatorName,
COALESCE(jopd.operatingEnd, jopd.operatingStart, jop.endTime, jo.planStart) AS _sort
FROM job_order_process_detail jopd
INNER JOIN equipment e ON e.id = jopd.equipmentId AND e.deleted = 0
INNER JOIN job_order_process jop ON jop.id = jopd.jopId AND jop.deleted = 0
INNER JOIN job_order jo ON jo.id = jop.joId AND jo.deleted = 0
INNER JOIN process p ON p.id = jop.processId AND p.deleted = 0
LEFT JOIN user u ON u.id = jopd.operatorId AND u.deleted = 0
WHERE jopd.deleted = 0
AND (
jop.endTime IS NOT NULL
OR LOWER(TRIM(COALESCE(jo.status, ''))) = 'completed'
)
AND COALESCE(jopd.operatingEnd, jopd.operatingStart, jop.endTime, jo.planStart) IS NOT NULL
AND DATE(COALESCE(jopd.operatingEnd, jopd.operatingStart, jop.endTime, jo.planStart)) = :eqUsageDate
UNION ALL
SELECT
-ppl.id AS jopdId,
COALESCE(ppl.equipmentId, 0) AS equipmentId,
COALESCE(NULLIF(TRIM(e.code), ''), '') AS equipmentCode,
COALESCE(
NULLIF(TRIM(CONCAT_WS('-',
NULLIF(TRIM(ppl.equipment_name), ''),
NULLIF(TRIM(COALESCE(e.name, '')), ''),
NULLIF(TRIM(COALESCE(ed.code, '')), '')
)), ''),
NULLIF(TRIM(COALESCE(e.name, '')), ''),
NULLIF(TRIM(ppl.equipment_name), ''),
'—'
) AS equipmentName,
jo.id AS jobOrderId,
jo.code AS jobOrderCode,
DATE_FORMAT(jo.planStart, '%Y-%m-%d %H:%i:%s') AS jobPlanStart,
p.code AS processCode,
p.name AS processName,
DATE_FORMAT(ppl.startTime, '%Y-%m-%d %H:%i:%s') AS operatingStart,
DATE_FORMAT(ppl.endTime, '%Y-%m-%d %H:%i:%s') AS operatingEnd,
CASE
WHEN ppl.startTime IS NOT NULL AND ppl.endTime IS NOT NULL
THEN GREATEST(0, TIMESTAMPDIFF(MINUTE, ppl.startTime, ppl.endTime))
WHEN ppl.startTime IS NOT NULL
AND LOWER(REPLACE(REPLACE(TRIM(COALESCE(ppl.status, '')), '_', ''), ' ', '')) IN ('completed', 'pass')
THEN GREATEST(0, COALESCE(ppl.processingTime, 0))
ELSE 0
END AS usageMinutes,
CASE
WHEN ppl.startTime IS NOT NULL AND ppl.endTime IS NULL THEN 1
ELSE 0
END AS workingNow,
COALESCE(u2.username, '') AS operatorUsername,
COALESCE(NULLIF(TRIM(COALESCE(u2.fullname, '')), ''), u2.name, u2.username, '') AS operatorName,
COALESCE(ppl.endTime, ppl.startTime, jop.endTime, jo.planStart) AS _sort
FROM productprocessline ppl
INNER JOIN productprocess pp ON pp.id = ppl.productprocessid AND pp.deleted = 0
INNER JOIN job_order jo ON jo.id = pp.jobOrderId AND jo.deleted = 0
INNER JOIN bom_process bp ON bp.id = ppl.bomProcessId AND bp.deleted = 0 AND bp.bomId = pp.bomId
INNER JOIN job_order_process jop ON jop.joId = jo.id
AND jop.processId = bp.processId
AND jop.seqNo = bp.seqNo
AND jop.deleted = 0
INNER JOIN process p ON p.id = jop.processId AND p.deleted = 0
LEFT JOIN equipment e ON e.id = ppl.equipmentId AND e.deleted = 0
LEFT JOIN equipment_detail ed ON ed.id = ppl.equipmentDetailId AND ed.deleted = 0
LEFT JOIN user u2 ON u2.id = ppl.operatorId AND u2.deleted = 0
WHERE ppl.deleted = 0
AND (
jop.endTime IS NOT NULL
OR ppl.endTime IS NOT NULL
OR LOWER(REPLACE(REPLACE(TRIM(COALESCE(ppl.status, '')), '_', ''), ' ', '')) IN ('completed', 'pass')
OR LOWER(TRIM(COALESCE(jo.status, ''))) = 'completed'
)
AND (
ppl.equipmentId IS NOT NULL
OR ppl.equipmentDetailId IS NOT NULL
OR NULLIF(TRIM(COALESCE(ppl.equipment_name, '')), '') IS NOT NULL
)
AND COALESCE(ppl.endTime, ppl.startTime, jop.endTime, jo.planStart) IS NOT NULL
AND DATE(COALESCE(ppl.endTime, ppl.startTime, jop.endTime, jo.planStart)) = :eqUsageDate
) t
ORDER BY t.workingNow DESC, t._sort DESC
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Process board: one row per [job_order_process] with master [process] and parent [job_order].
*
* [boardStatus] aligns with the job-order 工藝流程 UI (product process lines): we aggregate
* [productprocessline] rows for the same job order, BOM step ([bom_process] processId + seqNo),
* and fall back to [JobOrderProcess.startTime]/[endTime] when no lines match (legacy).
* Also returns line step name, description, equipment (type-name-code), and operator display name
* via [GROUP_CONCAT] when multiple lines match one [job_order_process] row.
*
* Same optional filters as [getJobOrderBoardRows]: plan-start day and incomplete jobs only.
*/
fun getProcessBoardRows(targetDate: LocalDate?, incompleteOnly: Boolean): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val dateSql = if (targetDate != null) {
args["procBoardDate"] = targetDate.toString()
"AND jo.planStart IS NOT NULL AND DATE(jo.planStart) = :procBoardDate"
} else ""
val incompleteSql = if (incompleteOnly) {
"AND LOWER(TRIM(COALESCE(jo.status, ''))) <> 'completed'"
} else ""
val sql = """
SELECT
jop.id AS jopId,
jo.id AS jobOrderId,
jo.code AS jobOrderCode,
COALESCE(jo.status, 'unknown') AS jobOrderStatus,
p.id AS processId,
p.code AS processCode,
p.name AS processName,
jop.seqNo AS seqNo,
COALESCE(jop.status, '') AS rowStatus,
DATE_FORMAT(jo.planStart, '%Y-%m-%d %H:%i:%s') AS jobPlanStart,
DATE_FORMAT(COALESCE(jop.startTime, lineAgg.minLineStart), '%Y-%m-%d %H:%i:%s') AS startTime,
DATE_FORMAT(COALESCE(jop.endTime, lineAgg.maxLineEndAllDone), '%Y-%m-%d %H:%i:%s') AS endTime,
COALESCE(lineAgg.lineStepName, p.name) AS lineStepName,
COALESCE(lineAgg.lineDescription, '') AS lineDescription,
COALESCE(lineAgg.lineEquipmentLabel, '') AS lineEquipmentLabel,
COALESCE(lineAgg.lineOperatorInfo, '') AS lineOperatorInfo,
CASE
WHEN jop.endTime IS NOT NULL THEN 'completed'
WHEN COALESCE(lineAgg.lineCount, 0) = 0 THEN
CASE
WHEN jop.startTime IS NOT NULL THEN 'in_progress'
ELSE 'pending'
END
WHEN lineAgg.notDoneCount = 0 THEN 'completed'
WHEN COALESCE(lineAgg.inProgressCount, 0) > 0 OR jop.startTime IS NOT NULL THEN 'in_progress'
ELSE 'pending'
END AS boardStatus,
COALESCE(it.code, '') AS itemCode,
COALESCE(it.name, '') AS itemName,
COALESCE(jt.name, '') AS jobTypeName,
jo.reqQty AS reqQty,
COALESCE(b.outputQtyUom, '') AS outputQtyUom,
(
SELECT DATE_FORMAT(ppd.date, '%Y-%m-%d')
FROM productprocess ppd
WHERE ppd.jobOrderId = jo.id AND ppd.deleted = 0
ORDER BY ppd.id ASC
LIMIT 1
) AS productionDate,
(
SELECT COALESCE(SUM(COALESCE(pplp.processingTime, 0)), 0)
FROM productprocessline pplp
INNER JOIN productprocess ppp ON ppp.id = pplp.productprocessid AND ppp.deleted = 0
WHERE ppp.jobOrderId = jo.id AND pplp.deleted = 0
) AS planProcessingMinsTotal,
(
SELECT COALESCE(SUM(
COALESCE(ppls.setupTime, 0) + COALESCE(ppls.changeoverTime, 0)
), 0)
FROM productprocessline ppls
INNER JOIN productprocess pps ON pps.id = ppls.productprocessid AND pps.deleted = 0
WHERE pps.jobOrderId = jo.id AND ppls.deleted = 0
) AS planSetupChangeoverMinsTotal,
(
SELECT DATE_FORMAT(MIN(ppst.startTime), '%Y-%m-%d %H:%i:%s')
FROM productprocess ppst
WHERE ppst.jobOrderId = jo.id AND ppst.deleted = 0 AND ppst.startTime IS NOT NULL
) AS productProcessStart,
(
SELECT ROUND(COALESCE(SUM(
CASE
WHEN pplx.startTime IS NOT NULL AND pplx.endTime IS NOT NULL
THEN GREATEST(0, TIMESTAMPDIFF(SECOND, pplx.startTime, pplx.endTime))
WHEN pplx.startTime IS NOT NULL
AND LOWER(REPLACE(REPLACE(TRIM(COALESCE(pplx.status, '')), '_', ''), ' ', '')) IN ('completed', 'pass')
THEN GREATEST(0, COALESCE(pplx.processingTime, 0) * 60)
ELSE 0
END
), 0) / 60.0, 2)
FROM productprocessline pplx
INNER JOIN productprocess ppx ON ppx.id = pplx.productprocessid AND ppx.deleted = 0
WHERE ppx.jobOrderId = jo.id AND pplx.deleted = 0
) AS actualLineMinsTotal,
COALESCE(lineAgg.stepPlanMins, 0) AS stepPlanMins,
COALESCE(lineAgg.stepActualMins, 0) AS stepActualMins
FROM job_order_process jop
INNER JOIN job_order jo ON jo.id = jop.joId AND jo.deleted = 0
INNER JOIN process p ON p.id = jop.processId AND p.deleted = 0
LEFT JOIN bom b ON b.id = jo.bomId AND b.deleted = 0
LEFT JOIN items it ON it.id = b.itemId AND it.deleted = 0
LEFT JOIN jobtype jt ON jt.id = jo.jobTypeId AND jt.deleted = 0
LEFT JOIN (
SELECT
pp.jobOrderId AS joId,
bp.processId AS processId,
bp.seqNo AS seqNo,
COUNT(*) AS lineCount,
SUM(
CASE
WHEN LOWER(REPLACE(REPLACE(TRIM(COALESCE(ppl.status, '')), '_', ''), ' ', '')) IN ('completed', 'pass')
OR ppl.endTime IS NOT NULL
THEN 0 ELSE 1
END
) AS notDoneCount,
SUM(
CASE
WHEN (
LOWER(REPLACE(REPLACE(TRIM(COALESCE(ppl.status, '')), '_', ''), ' ', '')) IN ('completed', 'pass')
OR ppl.endTime IS NOT NULL
) THEN 0
WHEN (
LOWER(REPLACE(REPLACE(TRIM(COALESCE(ppl.status, '')), '_', ''), ' ', '')) IN ('inprogress', 'paused')
OR (ppl.startTime IS NOT NULL AND ppl.endTime IS NULL)
) THEN 1
ELSE 0
END
) AS inProgressCount,
MIN(ppl.startTime) AS minLineStart,
CASE
WHEN SUM(
CASE
WHEN LOWER(REPLACE(REPLACE(TRIM(COALESCE(ppl.status, '')), '_', ''), ' ', '')) IN ('completed', 'pass')
OR ppl.endTime IS NOT NULL
THEN 0 ELSE 1
END
) = 0 AND COUNT(*) > 0
THEN MAX(ppl.endTime)
ELSE NULL
END AS maxLineEndAllDone,
GROUP_CONCAT(
NULLIF(TRIM(ppl.name), '')
ORDER BY ppl.seqNo ASC, ppl.id ASC
SEPARATOR ' | '
) AS lineStepName,
GROUP_CONCAT(
NULLIF(TRIM(ppl.description), '')
ORDER BY ppl.seqNo ASC, ppl.id ASC
SEPARATOR ' | '
) AS lineDescription,
GROUP_CONCAT(
NULLIF(TRIM(COALESCE(
NULLIF(TRIM(ed.code), ''),
NULLIF(TRIM(ppl.equipment_name), ''),
NULLIF(TRIM(e.name), '')
)), '')
ORDER BY ppl.seqNo ASC, ppl.id ASC
SEPARATOR ' | '
) AS lineEquipmentLabel,
GROUP_CONCAT(
NULLIF(TRIM(COALESCE(NULLIF(TRIM(COALESCE(u.fullname, '')), ''), u.name, u.username, '')), '')
ORDER BY ppl.seqNo ASC, ppl.id ASC
SEPARATOR ' | '
) AS lineOperatorInfo,
COALESCE(SUM(
COALESCE(ppl.processingTime, 0) + COALESCE(ppl.setupTime, 0) + COALESCE(ppl.changeoverTime, 0)
), 0) AS stepPlanMins,
ROUND(COALESCE(SUM(
CASE
WHEN ppl.startTime IS NOT NULL AND ppl.endTime IS NOT NULL
THEN GREATEST(0, TIMESTAMPDIFF(SECOND, ppl.startTime, ppl.endTime))
WHEN ppl.startTime IS NOT NULL
AND LOWER(REPLACE(REPLACE(TRIM(COALESCE(ppl.status, '')), '_', ''), ' ', '')) IN ('completed', 'pass')
THEN GREATEST(0, COALESCE(ppl.processingTime, 0) * 60)
ELSE 0
END
), 0) / 60.0, 2) AS stepActualMins
FROM productprocessline ppl
INNER JOIN productprocess pp ON pp.id = ppl.productprocessid AND pp.deleted = 0
INNER JOIN bom_process bp ON bp.id = ppl.bomProcessId AND bp.deleted = 0 AND bp.bomId = pp.bomId
LEFT JOIN equipment e ON e.id = ppl.equipmentId AND e.deleted = 0
LEFT JOIN equipment_detail ed ON ed.id = ppl.equipmentDetailId AND ed.deleted = 0
LEFT JOIN user u ON u.id = ppl.operatorId AND u.deleted = 0
WHERE ppl.deleted = 0
GROUP BY pp.jobOrderId, bp.processId, bp.seqNo
) lineAgg ON lineAgg.joId = jo.id
AND lineAgg.processId = jop.processId
AND lineAgg.seqNo = jop.seqNo
WHERE jop.deleted = 0
$dateSql
$incompleteSql
ORDER BY jo.planStart DESC, jo.code ASC, jop.seqNo ASC, jop.id ASC
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Job order board: one row per JO for visualization (planStart date filter when [targetDate] set).
* Stock-in metrics: only lines on jobs whose BOM is FG/WIP; splits QC 中 / 已驗待入庫 / 已入庫 by [stock_in_line].status.
* Material picked: status completed / linked stock out / or JO completed (BOM rows often stay pending if never bulk-updated).
* Process done: [job_order_process.endTime], or all [productprocessline] rows for the step Pass/Completed, or JO completed fallback.
*/
fun getJobOrderBoardRows(targetDate: LocalDate?, incompleteOnly: Boolean): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()
val dateSql = if (targetDate != null) {
args["boardTargetDate"] = targetDate.toString()
"AND jo.planStart IS NOT NULL AND DATE(jo.planStart) = :boardTargetDate"
} else ""
val incompleteSql = if (incompleteOnly) {
"AND LOWER(TRIM(COALESCE(jo.status, ''))) <> 'completed'"
} else ""
val sql = """
SELECT
jo.id AS jobOrderId,
jo.code AS code,
COALESCE(jo.status, 'unknown') AS status,
DATE_FORMAT(jo.planStart, '%Y-%m-%d %H:%i:%s') AS planStart,
DATE_FORMAT(jo.actualStart, '%Y-%m-%d %H:%i:%s') AS actualStart,
DATE_FORMAT(jo.planEnd, '%Y-%m-%d %H:%i:%s') AS planEnd,
DATE_FORMAT(jo.actualEnd, '%Y-%m-%d %H:%i:%s') AS actualEnd,
GREATEST(0, COALESCE(mat.matTotal, 0) - COALESCE(mat.pickedCnt, 0)) AS materialPendingCount,
COALESCE(mat.pickedCnt, 0) AS materialPickedCount,
COALESCE(proc.totalCnt, 0) AS processTotalCount,
COALESCE(proc.doneCnt, 0) AS processCompletedCount,
(
SELECT p.code
FROM job_order_process jop2
INNER JOIN process p ON p.id = jop2.processId AND p.deleted = 0
WHERE jop2.joId = jo.id AND jop2.deleted = 0 AND jop2.endTime IS NULL
AND LOWER(TRIM(COALESCE(jo.status, ''))) <> 'completed'
ORDER BY jop2.seqNo ASC, jop2.id ASC
LIMIT 1
) AS currentProcessCode,
(
SELECT p.name
FROM job_order_process jop2
INNER JOIN process p ON p.id = jop2.processId AND p.deleted = 0
WHERE jop2.joId = jo.id AND jop2.deleted = 0 AND jop2.endTime IS NULL
AND LOWER(TRIM(COALESCE(jo.status, ''))) <> 'completed'
ORDER BY jop2.seqNo ASC, jop2.id ASC
LIMIT 1
) AS currentProcessName,
(
SELECT DATE_FORMAT(jop2.startTime, '%Y-%m-%d %H:%i:%s')
FROM job_order_process jop2
WHERE jop2.joId = jo.id AND jop2.deleted = 0 AND jop2.endTime IS NULL
AND LOWER(TRIM(COALESCE(jo.status, ''))) <> 'completed'
ORDER BY jop2.seqNo ASC, jop2.id ASC
LIMIT 1
) AS currentProcessStartTime,
COALESCE(silAgg.stockInAcceptedQtyTotal, 0) AS stockInAcceptedQtyTotal,
COALESCE(silAgg.fgReadyToStockInCount, 0) AS fgReadyToStockInCount,
COALESCE(silAgg.fgReadyToStockInQty, 0) AS fgReadyToStockInQty,
COALESCE(silAgg.fgInQcLineCount, 0) AS fgInQcLineCount,
COALESCE(silAgg.fgInQcQty, 0) AS fgInQcQty,
COALESCE(silAgg.fgStockedQty, 0) AS fgStockedQty,
COALESCE(it.code, '') AS itemCode,
COALESCE(it.name, '') AS itemName,
COALESCE(jt.name, '') AS jobTypeName,
jo.reqQty AS reqQty,
COALESCE(b.outputQtyUom, '') AS outputQtyUom,
(
SELECT DATE_FORMAT(ppd.date, '%Y-%m-%d')
FROM productprocess ppd
WHERE ppd.jobOrderId = jo.id AND ppd.deleted = 0
ORDER BY ppd.id ASC
LIMIT 1
) AS productionDate,
(
SELECT COALESCE(SUM(COALESCE(pplp.processingTime, 0)), 0)
FROM productprocessline pplp
INNER JOIN productprocess ppp ON ppp.id = pplp.productprocessid AND ppp.deleted = 0
WHERE ppp.jobOrderId = jo.id AND pplp.deleted = 0
) AS planProcessingMinsTotal,
(
SELECT COALESCE(SUM(
COALESCE(ppls.setupTime, 0) + COALESCE(ppls.changeoverTime, 0)
), 0)
FROM productprocessline ppls
INNER JOIN productprocess pps ON pps.id = ppls.productprocessid AND pps.deleted = 0
WHERE pps.jobOrderId = jo.id AND ppls.deleted = 0
) AS planSetupChangeoverMinsTotal,
(
SELECT DATE_FORMAT(MIN(ppst.startTime), '%Y-%m-%d %H:%i:%s')
FROM productprocess ppst
WHERE ppst.jobOrderId = jo.id AND ppst.deleted = 0 AND ppst.startTime IS NOT NULL
) AS productProcessStart,
(
SELECT ROUND(COALESCE(SUM(
CASE
WHEN pplx.startTime IS NOT NULL AND pplx.endTime IS NOT NULL
THEN GREATEST(0, TIMESTAMPDIFF(SECOND, pplx.startTime, pplx.endTime))
WHEN pplx.startTime IS NOT NULL
AND LOWER(REPLACE(REPLACE(TRIM(COALESCE(pplx.status, '')), '_', ''), ' ', '')) IN ('completed', 'pass')
THEN GREATEST(0, COALESCE(pplx.processingTime, 0) * 60)
ELSE 0
END
), 0) / 60.0, 2)
FROM productprocessline pplx
INNER JOIN productprocess ppx ON ppx.id = pplx.productprocessid AND ppx.deleted = 0
WHERE ppx.jobOrderId = jo.id AND pplx.deleted = 0
) AS actualLineMinsTotal
FROM job_order jo
LEFT JOIN bom b ON b.id = jo.bomId AND b.deleted = 0
LEFT JOIN items it ON it.id = b.itemId AND it.deleted = 0
LEFT JOIN jobtype jt ON jt.id = jo.jobTypeId AND jt.deleted = 0
LEFT JOIN (
SELECT
jobm.jobOrderId AS joId,
COUNT(jobm.id) AS matTotal,
COALESCE(SUM(CASE
WHEN LOWER(TRIM(COALESCE(jobm.status, ''))) IN ('completed', 'complete')
OR jobm.stockOutLineId IS NOT NULL
OR LOWER(TRIM(COALESCE(joMat.status, ''))) = 'completed'
THEN 1 ELSE 0 END), 0) AS pickedCnt
FROM job_order_bom_material jobm
INNER JOIN job_order joMat ON joMat.id = jobm.jobOrderId AND joMat.deleted = 0
WHERE jobm.deleted = 0
GROUP BY jobm.jobOrderId
) mat ON mat.joId = jo.id
LEFT JOIN (
SELECT
jop.joId AS joId,
COUNT(jop.id) AS totalCnt,
COALESCE(SUM(CASE
WHEN jop.endTime IS NOT NULL THEN 1
WHEN COALESCE(lineAgg.lineCount, 0) > 0 AND lineAgg.notDoneCount = 0 THEN 1
WHEN LOWER(TRIM(COALESCE(joProc.status, ''))) = 'completed' THEN 1
ELSE 0 END), 0) AS doneCnt
FROM job_order_process jop
INNER JOIN job_order joProc ON joProc.id = jop.joId AND joProc.deleted = 0
LEFT JOIN (
SELECT
pp.jobOrderId AS joId,
bp.processId AS processId,
bp.seqNo AS seqNo,
COUNT(*) AS lineCount,
SUM(
CASE
WHEN LOWER(REPLACE(REPLACE(TRIM(COALESCE(ppl.status, '')), '_', ''), ' ', '')) IN ('completed', 'pass')
OR ppl.endTime IS NOT NULL
THEN 0 ELSE 1
END
) AS notDoneCount
FROM productprocessline ppl
INNER JOIN productprocess pp ON pp.id = ppl.productprocessid AND pp.deleted = 0
INNER JOIN bom_process bp ON bp.id = ppl.bomProcessId AND bp.deleted = 0 AND bp.bomId = pp.bomId
WHERE ppl.deleted = 0
GROUP BY pp.jobOrderId, bp.processId, bp.seqNo
) lineAgg ON lineAgg.joId = jop.joId
AND lineAgg.processId = jop.processId
AND lineAgg.seqNo = jop.seqNo
WHERE jop.deleted = 0
GROUP BY jop.joId
) proc ON proc.joId = jo.id
LEFT JOIN (
SELECT
sil.jobOrderId AS joId,
COALESCE(SUM(COALESCE(sil.acceptedQty, 0)), 0) AS stockInAcceptedQtyTotal,
COALESCE(SUM(CASE WHEN LOWER(COALESCE(sil.status, '')) IN ('receiving', 'received') THEN 1 ELSE 0 END), 0) AS fgReadyToStockInCount,
COALESCE(SUM(CASE WHEN LOWER(COALESCE(sil.status, '')) IN ('receiving', 'received') THEN COALESCE(sil.acceptedQty, 0) ELSE 0 END), 0) AS fgReadyToStockInQty,
COALESCE(SUM(CASE WHEN LOWER(COALESCE(sil.status, '')) IN (
'pending', 'qc1', 'qc2', 'qc3', 'qc', 'escalated',
'determine1', 'determine2', 'determine3'
) THEN 1 ELSE 0 END), 0) AS fgInQcLineCount,
COALESCE(SUM(CASE WHEN LOWER(COALESCE(sil.status, '')) IN (
'pending', 'qc1', 'qc2', 'qc3', 'qc', 'escalated',
'determine1', 'determine2', 'determine3'
) THEN COALESCE(sil.acceptedQty, 0) ELSE 0 END), 0) AS fgInQcQty,
COALESCE(SUM(CASE WHEN LOWER(COALESCE(sil.status, '')) IN ('completed', 'complete', 'partially_completed') THEN COALESCE(sil.acceptedQty, 0) ELSE 0 END), 0) AS fgStockedQty
FROM stock_in_line sil
INNER JOIN job_order joSil ON joSil.id = sil.jobOrderId AND joSil.deleted = 0
INNER JOIN bom b ON b.id = joSil.bomId AND b.deleted = 0
AND UPPER(TRIM(COALESCE(b.description, ''))) IN ('FG', 'WIP')
WHERE sil.deleted = 0 AND sil.jobOrderId IS NOT NULL
GROUP BY sil.jobOrderId
) silAgg ON silAgg.joId = jo.id
WHERE jo.deleted = 0
$dateSql
$incompleteSql
ORDER BY jo.planStart DESC, jo.code
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

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

/**


+ 31
- 0
src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt Просмотреть файл

@@ -246,6 +246,37 @@ class ChartController(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) endDate: LocalDate?,
): List<Map<String, Any>> = chartService.getJobEquipmentWorkingWorkedByDate(startDate, endDate)

/**
* GET /chart/equipment-usage-board?targetDate=
* — equipment usage for the day: [job_order_process_detail] (legacy) **union** [productprocessline] (工藝流程),
* matching `/jo/edit` line equipment. Omit targetDate = server today.
*/
@GetMapping("/equipment-usage-board")
fun getEquipmentUsageBoard(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) targetDate: LocalDate?,
): List<Map<String, Any>> = chartService.getEquipmentUsageBoardRows(targetDate)

/**
* GET /chart/job-order-board?targetDate=&incompleteOnly=
* — per-job rows for status board (optional planStart date; incompleteOnly excludes status completed).
*/
@GetMapping("/job-order-board")
fun getJobOrderBoard(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) targetDate: LocalDate?,
@RequestParam(required = false, defaultValue = "false") incompleteOnly: Boolean,
): List<Map<String, Any>> = chartService.getJobOrderBoardRows(targetDate, incompleteOnly)

/**
* GET /chart/process-board?targetDate=&incompleteOnly=
* — per job_order_process row: job, master process, times, derived boardStatus (pending / in_progress / completed)
* from product process lines when job_order_process times are unset.
*/
@GetMapping("/process-board")
fun getProcessBoard(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) targetDate: LocalDate?,
@RequestParam(required = false, defaultValue = "false") incompleteOnly: Boolean,
): List<Map<String, Any>> = chartService.getProcessBoardRows(targetDate, incompleteOnly)

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

/** GET /chart/production-schedule-by-date?startDate=&endDate= — schedule count & total est prod by produce date. */


Загрузка…
Отмена
Сохранить