Bladeren bron

added some purchase chart

master
Fai Luk 3 dagen geleden
bovenliggende
commit
d83454042d
4 gewijzigde bestanden met toevoegingen van 532 en 4 verwijderingen
  1. +20
    -0
      .cursor/rules/frontend-location.mdc
  2. +12
    -0
      AGENTS.md
  3. +415
    -2
      src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt
  4. +85
    -2
      src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt

+ 20
- 0
.cursor/rules/frontend-location.mdc Bestand weergeven

@@ -0,0 +1,20 @@
---
description: Frontend repo location and cross-repo workflow
alwaysApply: true
---

# Frontend Location

- Backend repository: `FPSMS-backend`
- Frontend repository: `../FPSMS-frontend`

# When a request involves UI behavior

- Check whether the change belongs to backend, frontend, or both.
- If chart/page click behavior is requested, update frontend handlers in `../FPSMS-frontend`.
- Keep API contracts consistent between backend endpoints and frontend client calls.

# Cross-repo coordination

- When adding backend endpoints used by frontend, also add/update frontend API client functions.
- Validate impacted repo(s): backend compile/tests and frontend type-check/lint as needed.

+ 12
- 0
AGENTS.md Bestand weergeven

@@ -0,0 +1,12 @@
# FPSMS Agent Notes

## Repository Relationship

- Current backend repo: `FPSMS-backend`
- Paired frontend repo location: `../FPSMS-frontend`

## Working Convention

- If a task is about UI behavior (charts, clicks, page rendering, dialogs), check `../FPSMS-frontend`.
- If a task is about API/business logic/data query, check this backend repo.
- For end-to-end changes, update both repos and keep API request/response fields aligned.

+ 415
- 2
src/main/java/com/ffii/fpsms/modules/chart/service/ChartService.kt Bestand weergeven

@@ -69,26 +69,439 @@ open class ChartService(
/**
* Purchase orders: count by status (pending, receiving, completed).
* targetDate: when set, only POs whose orderDate is on that date; when null, all POs.
* Optional multi-filters: supplierIds, itemCodes (line itemNo), purchaseOrderNos (po.code).
*/
fun getPurchaseOrderByStatus(targetDate: LocalDate?): List<Map<String, Any>> {
fun getPurchaseOrderByStatus(
targetDate: LocalDate?,
supplierIds: List<Long>?,
itemCodes: List<String>?,
purchaseOrderNos: List<String>?,
): 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 multiSql = buildPoMultiFiltersSql(supplierIds, itemCodes, purchaseOrderNos, null, args)
val sql = """
SELECT
COALESCE(po.status, 'unknown') AS status,
COUNT(po.id) AS count
FROM purchase_order po
WHERE po.deleted = 0
$dateSql
$dateSql $multiSql
GROUP BY po.status
ORDER BY count DESC
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Filter options for purchase chart (distinct suppliers / items / PO numbers on orderDate).
*/
fun getPurchaseOrderFilterOptions(targetDate: LocalDate?): Map<String, Any> {
if (targetDate == null) {
return mapOf(
"suppliers" to emptyList<Any>(),
"items" to emptyList<Any>(),
"poNos" to emptyList<Any>(),
)
}
val args = mutableMapOf<String, Any>("targetDate" to targetDate.toString())
val suppliersSql = """
SELECT DISTINCT po.supplierId AS supplierId, s.code AS code, COALESCE(s.name, '') AS name
FROM purchase_order po
LEFT JOIN shop s ON s.id = po.supplierId AND s.deleted = 0
WHERE po.deleted = 0 AND DATE(po.orderDate) = :targetDate AND po.supplierId IS NOT NULL
ORDER BY s.code
""".trimIndent()
val itemsSql = """
SELECT DISTINCT pol.itemNo AS itemCode, COALESCE(it.name, '') AS itemName
FROM purchase_order_line pol
INNER JOIN purchase_order po ON po.id = pol.purchaseOrderId AND po.deleted = 0
LEFT JOIN items it ON it.id = pol.itemId AND it.deleted = 0
WHERE pol.deleted = 0 AND DATE(po.orderDate) = :targetDate
ORDER BY pol.itemNo
""".trimIndent()
val poNosSql = """
SELECT DISTINCT po.code AS poNo
FROM purchase_order po
WHERE po.deleted = 0 AND DATE(po.orderDate) = :targetDate
ORDER BY po.code
""".trimIndent()
return mapOf(
"suppliers" to jdbcDao.queryForList(suppliersSql, args),
"items" to jdbcDao.queryForList(itemsSql, args),
"poNos" to jdbcDao.queryForList(poNosSql, args),
)
}

/**
* 預計送貨: POs with estimatedArrivalDate on targetDate, grouped by 已送 / 未送 / 已取消.
* 已取消: deleted = 1; 已送: deleted = 0 and status = completed; 未送: deleted = 0 and pending/receiving.
* other: remaining active statuses (if any).
*/
fun getPurchaseOrderEstimatedArrivalSummary(
targetDate: LocalDate?,
supplierIds: List<Long>?,
itemCodes: List<String>?,
purchaseOrderNos: List<String>?,
): List<Map<String, Any>> {
if (targetDate == null) return emptyList()
val args = mutableMapOf<String, Any>("targetDate" to targetDate.toString())
val multiSql = buildPoMultiFiltersSql(supplierIds, itemCodes, purchaseOrderNos, null, args)
val sql = """
SELECT
CASE
WHEN COALESCE(po.deleted, 0) = 1 THEN 'cancelled'
WHEN COALESCE(po.deleted, 0) = 0 AND LOWER(COALESCE(po.status, '')) = 'completed' THEN 'delivered'
WHEN COALESCE(po.deleted, 0) = 0 AND LOWER(COALESCE(po.status, '')) IN ('pending', 'receiving') THEN 'not_delivered'
ELSE 'other'
END AS bucket,
COUNT(DISTINCT po.id) AS count
FROM purchase_order po
WHERE DATE(po.estimatedArrivalDate) = :targetDate
AND po.estimatedArrivalDate IS NOT NULL
$multiSql
GROUP BY bucket
ORDER BY count DESC
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* 預計送貨: for a single bucket (delivered / not_delivered / cancelled / other) on [targetDate]
* (DATE(estimatedArrivalDate)), return distinct suppliers, items, and PO headers matching the same rules as
* [getPurchaseOrderEstimatedArrivalSummary] plus [buildPoMultiFiltersSql] bar filters.
*/
fun getPurchaseOrderEstimatedArrivalBreakdown(
targetDate: LocalDate?,
estimatedArrivalBucket: String?,
supplierIds: List<Long>?,
itemCodes: List<String>?,
purchaseOrderNos: List<String>?,
): Map<String, Any> {
if (targetDate == null || estimatedArrivalBucket.isNullOrBlank()) {
return mapOf(
"suppliers" to emptyList<Any>(),
"items" to emptyList<Any>(),
"purchaseOrders" to emptyList<Any>(),
)
}
val args = mutableMapOf<String, Any>()
val bucketSql = buildPoDeletedAndEstimatedArrivalBucketSql(estimatedArrivalBucket, targetDate, args)
val multiSql = buildPoMultiFiltersSql(supplierIds, itemCodes, purchaseOrderNos, null, args)
val suppliersSql = """
SELECT
po.supplierId AS supplierId,
s.code AS supplierCode,
COALESCE(s.name, '') AS supplierName,
COUNT(DISTINCT po.id) AS poCount
FROM purchase_order po
LEFT JOIN shop s ON s.id = po.supplierId AND s.deleted = 0
WHERE $bucketSql
$multiSql
GROUP BY po.supplierId, s.code, s.name
ORDER BY poCount DESC, supplierCode
""".trimIndent()
val itemsSql = """
SELECT
pol.itemNo AS itemCode,
COALESCE(it.name, '') AS itemName,
COUNT(DISTINCT po.id) AS poCount,
COALESCE(SUM(pol.qty), 0) AS totalQty
FROM purchase_order po
INNER JOIN purchase_order_line pol ON pol.purchaseOrderId = po.id AND pol.deleted = 0
LEFT JOIN items it ON it.id = pol.itemId AND it.deleted = 0
WHERE $bucketSql
$multiSql
GROUP BY pol.itemNo, it.name
ORDER BY totalQty DESC, pol.itemNo
""".trimIndent()
val posSql = """
SELECT
po.id AS purchaseOrderId,
po.code AS purchaseOrderNo,
COALESCE(po.status, '') AS status,
DATE_FORMAT(po.orderDate, '%Y-%m-%d') AS orderDate,
po.supplierId AS supplierId,
s.code AS supplierCode,
COALESCE(s.name, '') AS supplierName
FROM purchase_order po
LEFT JOIN shop s ON s.id = po.supplierId AND s.deleted = 0
WHERE $bucketSql
$multiSql
ORDER BY po.code
""".trimIndent()
return mapOf(
"suppliers" to jdbcDao.queryForList(suppliersSql, args),
"items" to jdbcDao.queryForList(itemsSql, args),
"purchaseOrders" to jdbcDao.queryForList(posSql, args),
)
}

private fun buildPoMultiFiltersSql(
supplierIds: List<Long>?,
itemCodes: List<String>?,
purchaseOrderNos: List<String>?,
supplierCode: String?,
args: MutableMap<String, Any>,
): String {
val sb = StringBuilder()
val sids = supplierIds?.mapNotNull { it }?.filter { it > 0 }?.distinct() ?: emptyList()
val scode = supplierCode?.trim()?.takeIf { it.isNotEmpty() }
if (sids.isNotEmpty()) {
args["filterSupplierIds"] = sids
sb.append(" AND po.supplierId IN (:filterSupplierIds)")
} else if (scode != null) {
args["filterSupplierCode"] = scode
sb.append(
" AND EXISTS (SELECT 1 FROM shop sfc WHERE sfc.id = po.supplierId AND sfc.deleted = 0 AND sfc.code = :filterSupplierCode)"
)
}
val icodes = itemCodes?.map { it.trim() }?.filter { it.isNotEmpty() }?.distinct() ?: emptyList()
if (icodes.isNotEmpty()) {
args["filterItemCodes"] = icodes
sb.append(
""" AND EXISTS (
SELECT 1 FROM purchase_order_line polf
WHERE polf.purchaseOrderId = po.id AND polf.deleted = 0 AND polf.itemNo IN (:filterItemCodes)
)"""
)
}
val poNos = purchaseOrderNos?.map { it.trim() }?.filter { it.isNotEmpty() }?.distinct() ?: emptyList()
if (poNos.isNotEmpty()) {
args["filterPoNos"] = poNos
sb.append(" AND po.code IN (:filterPoNos)")
}
return sb.toString()
}

/**
* Purchase orders detail rows by status for chart drill-down.
* Example use: click "pending" segment -> show matching PO headers.
* targetDate: optional filter — by order date or complete date (see dateFilter).
* dateFilter: "order" = DATE(po.orderDate); "complete" = DATE(po.completeDate) (for received/completed on a day).
* supplierIds / itemCodes / purchaseOrderNos: optional multi-filters (same as chart filter bar).
* supplierCode: optional when supplierId list is empty (e.g. drill-down with code only).
*/
fun getPurchaseOrderDetailsByStatus(
status: String,
targetDate: LocalDate?,
dateFilter: String,
supplierIds: List<Long>?,
itemCodes: List<String>?,
purchaseOrderNos: List<String>?,
supplierCode: String?,
estimatedArrivalBucket: String?,
): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>(
"status" to status.trim().lowercase()
)
val dateSql = buildPurchaseOrderDrillOrderDateSql(estimatedArrivalBucket, targetDate, dateFilter, args)
val multiSql = buildPoMultiFiltersSql(supplierIds, itemCodes, purchaseOrderNos, supplierCode, args)
val deletedAndEaSql = buildPoDeletedAndEstimatedArrivalBucketSql(estimatedArrivalBucket, targetDate, args)
/** When 預計送貨 bucket is active, [buildPoDeletedAndEstimatedArrivalBucketSql] already fixes status shape (e.g. 未送 = pending|receiving). Do not AND with :status (would wrongly require e.g. completed). */
val statusSingleFilterSql = if (estimatedArrivalBucket.isNullOrBlank() || targetDate == null) {
"AND LOWER(COALESCE(po.status, 'unknown')) = :status"
} else {
""
}
val sql = """
SELECT
po.id AS purchaseOrderId,
po.code AS purchaseOrderNo,
COALESCE(po.status, 'unknown') AS status,
DATE_FORMAT(po.orderDate, '%Y-%m-%d') AS orderDate,
DATE_FORMAT(po.estimatedArrivalDate, '%Y-%m-%d') AS estimatedArrivalDate,
po.supplierId AS supplierId,
s.code AS supplierCode,
COALESCE(s.name, '') AS supplierName,
COUNT(DISTINCT pol.id) AS itemCount,
COALESCE(SUM(pol.qty), 0) AS totalQty
FROM purchase_order po
LEFT JOIN purchase_order_line pol ON pol.purchaseOrderId = po.id AND pol.deleted = 0
LEFT JOIN shop s ON s.id = po.supplierId AND s.deleted = 0
WHERE $deletedAndEaSql
$statusSingleFilterSql
$dateSql $multiSql
GROUP BY
po.id, po.code, po.status, po.orderDate, po.estimatedArrivalDate, po.supplierId, s.code, s.name
ORDER BY po.orderDate DESC, po.code DESC
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* When [estimatedArrivalBucket] is set (delivered / not_delivered / cancelled / other), filter by
* [getPurchaseOrderEstimatedArrivalSummary] bucket rules on [targetDate] (estimated arrival date).
* Includes soft-deleted rows when bucket is cancelled.
*/
private fun buildPoDeletedAndEstimatedArrivalBucketSql(
estimatedArrivalBucket: String?,
targetDate: LocalDate?,
args: MutableMap<String, Any>,
): String {
val b = estimatedArrivalBucket?.trim()?.lowercase()?.takeIf { it.isNotEmpty() }
if (b == null || targetDate == null) {
return "po.deleted = 0"
}
args["eaBucket"] = b
args["eaTargetDate"] = targetDate.toString()
return """
1=1
AND po.estimatedArrivalDate IS NOT NULL
AND DATE(po.estimatedArrivalDate) = :eaTargetDate
AND (
CASE
WHEN COALESCE(po.deleted, 0) = 1 THEN 'cancelled'
WHEN COALESCE(po.deleted, 0) = 0 AND LOWER(COALESCE(po.status, '')) = 'completed' THEN 'delivered'
WHEN COALESCE(po.deleted, 0) = 0 AND LOWER(COALESCE(po.status, '')) IN ('pending', 'receiving') THEN 'not_delivered'
ELSE 'other'
END
) = :eaBucket
""".trimIndent()
}

private fun buildPurchaseOrderTargetDateSql(
targetDate: LocalDate?,
dateFilter: String,
args: MutableMap<String, Any>,
): String {
if (targetDate == null) return ""
args["targetDate"] = targetDate.toString()
return if (dateFilter.equals("complete", ignoreCase = true)) {
"AND po.completeDate IS NOT NULL AND DATE(po.completeDate) = :targetDate"
} else {
"AND DATE(po.orderDate) = :targetDate"
}
}

/**
* Drill-down rows for 預計送貨 must match [getPurchaseOrderEstimatedArrivalSummary] (estimated arrival date only).
* If we also applied [buildPurchaseOrderTargetDateSql], many POs (orderDate ≠ 預計到貨日) would drop out and
* supplier / item charts would collapse incorrectly.
*
* For 實際已送貨 (no bucket), [getPurchaseOrderByStatus] is always filtered by **order date** only. If drill used
* [dateFilter] = complete, rows would be restricted to completeDate = targetDate and supplier/item charts would
* collapse to a tiny set (often one supplier) vs the donut counts.
*/
private fun buildPurchaseOrderDrillOrderDateSql(
estimatedArrivalBucket: String?,
targetDate: LocalDate?,
@Suppress("UNUSED_PARAMETER") dateFilter: String,
args: MutableMap<String, Any>,
): String {
if (!estimatedArrivalBucket.isNullOrBlank() && targetDate != null) {
return ""
}
return buildPurchaseOrderTargetDateSql(targetDate, "order", args)
}

/**
* Purchase order item lines for drill-down.
* Includes ordered qty, received qty and pending qty.
*/
fun getPurchaseOrderItems(purchaseOrderId: Long): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>("purchaseOrderId" to purchaseOrderId)
val sql = """
SELECT
pol.id AS purchaseOrderLineId,
pol.itemNo AS itemCode,
COALESCE(it.name, '') AS itemName,
COALESCE(pol.qty, 0) AS orderedQty,
COALESCE(u.udfudesc, u.code, u.udfShortDesc, '') AS uom,
COALESCE(SUM(
CASE
WHEN sil.id IS NOT NULL THEN COALESCE(sil.acceptedQty, 0)
ELSE 0
END
), 0) AS receivedQty,
GREATEST(
COALESCE(pol.qty, 0) - COALESCE(SUM(
CASE
WHEN sil.id IS NOT NULL THEN COALESCE(sil.acceptedQty, 0)
ELSE 0
END
), 0),
0
) AS pendingQty
FROM purchase_order_line pol
INNER JOIN purchase_order po ON po.id = pol.purchaseOrderId AND po.deleted = 0
LEFT JOIN items it ON it.id = pol.itemId AND it.deleted = 0
LEFT JOIN uom_conversion u ON u.id = pol.uomId
LEFT JOIN stock_in_line sil ON sil.purchaseOrderLineId = pol.id AND sil.deleted = 0
WHERE pol.deleted = 0
AND pol.purchaseOrderId = :purchaseOrderId
GROUP BY pol.id, pol.itemNo, it.name, pol.qty, u.udfudesc, u.code, u.udfShortDesc
ORDER BY pol.id
""".trimIndent()
return jdbcDao.queryForList(sql, args)
}

/**
* Purchase order items grouped by item for a status (and optional targetDate).
* Used for default drill-down chart when no PO is selected.
* supplierIds / itemCodes / purchaseOrderNos: optional multi-filters.
* supplierCode: optional when supplierId list is empty (e.g. drill-down with code only).
*/
fun getPurchaseOrderItemsByStatus(
status: String,
targetDate: LocalDate?,
dateFilter: String,
supplierIds: List<Long>?,
itemCodes: List<String>?,
purchaseOrderNos: List<String>?,
supplierCode: String?,
estimatedArrivalBucket: String?,
): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>(
"status" to status.trim().lowercase()
)
val dateSql = buildPurchaseOrderDrillOrderDateSql(estimatedArrivalBucket, targetDate, dateFilter, args)
val multiSql = buildPoMultiFiltersSql(supplierIds, itemCodes, purchaseOrderNos, supplierCode, args)
val deletedAndEaSql = buildPoDeletedAndEstimatedArrivalBucketSql(estimatedArrivalBucket, targetDate, args)
val statusSingleFilterSql = if (estimatedArrivalBucket.isNullOrBlank() || targetDate == null) {
"AND LOWER(COALESCE(po.status, 'unknown')) = :status"
} else {
""
}
val sql = """
SELECT
pol.itemNo AS itemCode,
COALESCE(it.name, '') AS itemName,
COALESCE(u.udfudesc, u.code, u.udfShortDesc, '') AS uom,
COALESCE(SUM(pol.qty), 0) AS orderedQty,
COALESCE(SUM(
CASE
WHEN sil.id IS NOT NULL THEN COALESCE(sil.acceptedQty, 0)
ELSE 0
END
), 0) AS receivedQty,
GREATEST(
COALESCE(SUM(pol.qty), 0) - COALESCE(SUM(
CASE
WHEN sil.id IS NOT NULL THEN COALESCE(sil.acceptedQty, 0)
ELSE 0
END
), 0),
0
) AS pendingQty
FROM purchase_order po
INNER JOIN purchase_order_line pol ON pol.purchaseOrderId = po.id AND pol.deleted = 0
LEFT JOIN shop s ON s.id = po.supplierId AND s.deleted = 0
LEFT JOIN items it ON it.id = pol.itemId AND it.deleted = 0
LEFT JOIN uom_conversion u ON u.id = pol.uomId
LEFT JOIN stock_in_line sil ON sil.purchaseOrderLineId = pol.id AND sil.deleted = 0
WHERE $deletedAndEaSql
$statusSingleFilterSql
$dateSql $multiSql
GROUP BY pol.itemNo, it.name, u.udfudesc, u.code, u.udfShortDesc
ORDER BY orderedQty DESC, pol.itemNo
""".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.


+ 85
- 2
src/main/java/com/ffii/fpsms/modules/chart/web/ChartController.kt Bestand weergeven

@@ -35,14 +35,97 @@ class ChartController(
): List<Map<String, Any>> = chartService.getDeliveryOrderByDate(startDate, endDate)

/**
* GET /chart/purchase-order-by-status?targetDate=2025-03-15
* GET /chart/purchase-order-by-status?targetDate=2025-03-15&supplierId=1&supplierId=2&itemCode=A&purchaseOrderNo=PO001
* 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?,
@RequestParam(required = false) supplierId: List<Long>?,
@RequestParam(required = false) itemCode: List<String>?,
@RequestParam(required = false) purchaseOrderNo: List<String>?,
): List<Map<String, Any>> =
chartService.getPurchaseOrderByStatus(targetDate, supplierId, itemCode, purchaseOrderNo)

/**
* GET /chart/purchase-order-filter-options?targetDate=2025-03-15
* Returns { suppliers, items, poNos } for multi-select (POs with orderDate on targetDate, deleted = 0).
*/
@GetMapping("/purchase-order-filter-options")
fun getPurchaseOrderFilterOptions(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) targetDate: LocalDate?,
): Map<String, Any> = chartService.getPurchaseOrderFilterOptions(targetDate)

/**
* GET /chart/purchase-order-estimated-arrival-summary?targetDate=2025-03-15
* 預計送貨: POs with estimatedArrivalDate on targetDate; buckets cancelled / delivered / not_delivered / other.
*/
@GetMapping("/purchase-order-estimated-arrival-summary")
fun getPurchaseOrderEstimatedArrivalSummary(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) targetDate: LocalDate?,
@RequestParam(required = false) supplierId: List<Long>?,
@RequestParam(required = false) itemCode: List<String>?,
@RequestParam(required = false) purchaseOrderNo: List<String>?,
): List<Map<String, Any>> =
chartService.getPurchaseOrderEstimatedArrivalSummary(targetDate, supplierId, itemCode, purchaseOrderNo)

/**
* GET /chart/purchase-order-estimated-arrival-breakdown?targetDate=&estimatedArrivalBucket=not_delivered
* Related suppliers / items / POs for that 預計送貨 segment (same filters as summary).
*/
@GetMapping("/purchase-order-estimated-arrival-breakdown")
fun getPurchaseOrderEstimatedArrivalBreakdown(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) targetDate: LocalDate,
@RequestParam estimatedArrivalBucket: String,
@RequestParam(required = false) supplierId: List<Long>?,
@RequestParam(required = false) itemCode: List<String>?,
@RequestParam(required = false) purchaseOrderNo: List<String>?,
): Map<String, Any> =
chartService.getPurchaseOrderEstimatedArrivalBreakdown(targetDate, estimatedArrivalBucket, supplierId, itemCode, purchaseOrderNo)

/**
* GET /chart/purchase-order-details-by-status?status=pending&targetDate=2025-03-15
* Returns PO header rows for status drill-down.
*/
@GetMapping("/purchase-order-details-by-status")
fun getPurchaseOrderDetailsByStatus(
@RequestParam status: String,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) targetDate: LocalDate?,
@RequestParam(required = false, defaultValue = "order") dateFilter: String,
@RequestParam(required = false) supplierId: List<Long>?,
@RequestParam(required = false) itemCode: List<String>?,
@RequestParam(required = false) purchaseOrderNo: List<String>?,
@RequestParam(required = false) supplierCode: String?,
@RequestParam(required = false) estimatedArrivalBucket: String?,
): List<Map<String, Any>> =
chartService.getPurchaseOrderDetailsByStatus(status, targetDate, dateFilter, supplierId, itemCode, purchaseOrderNo, supplierCode, estimatedArrivalBucket)

/**
* GET /chart/purchase-order-items?purchaseOrderId=123
* Returns PO item line rows with ordered/received/pending qty and uom.
*/
@GetMapping("/purchase-order-items")
fun getPurchaseOrderItems(
@RequestParam purchaseOrderId: Long,
): List<Map<String, Any>> =
chartService.getPurchaseOrderItems(purchaseOrderId)

/**
* GET /chart/purchase-order-items-by-status?status=completed&targetDate=2025-03-15
* Returns item rows aggregated across matching purchase orders.
*/
@GetMapping("/purchase-order-items-by-status")
fun getPurchaseOrderItemsByStatus(
@RequestParam status: String,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) targetDate: LocalDate?,
@RequestParam(required = false, defaultValue = "order") dateFilter: String,
@RequestParam(required = false) supplierId: List<Long>?,
@RequestParam(required = false) itemCode: List<String>?,
@RequestParam(required = false) purchaseOrderNo: List<String>?,
@RequestParam(required = false) supplierCode: String?,
@RequestParam(required = false) estimatedArrivalBucket: String?,
): List<Map<String, Any>> =
chartService.getPurchaseOrderByStatus(targetDate)
chartService.getPurchaseOrderItemsByStatus(status, targetDate, dateFilter, supplierId, itemCode, purchaseOrderNo, supplierCode, estimatedArrivalBucket)

/**
* GET /chart/stock-in-out-by-date?startDate=&endDate=


Laden…
Annuleren
Opslaan