From 620bb4712a31791231c4381581f0f60088f079fe Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 27 五月 2026 14:03:45 +0800
Subject: [PATCH] Merge branch 'refs/heads/dev_New_pro' into dev_宁夏_英泽防锈

---
 src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java |  209 ++++++++++++++++++++++++++++++++++++---------------
 1 files changed, 147 insertions(+), 62 deletions(-)

diff --git a/src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java b/src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java
index 900242b..e907aff 100644
--- a/src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java
+++ b/src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java
@@ -88,7 +88,14 @@
     private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
     private static final int DEFAULT_LIMIT = 10;
     private static final int MAX_LIMIT = 50;
-    private static final BigDecimal DEFAULT_FALLBACK_MATERIAL_COST_RATE = new BigDecimal("0.65");
+
+    private static final BigDecimal DEFAULT_FALLBACK_MATERIAL_COST_RATE = new BigDecimal("0.60");
+    private static final BigDecimal DEFAULT_LABOR_COST_RATE = new BigDecimal("0.15");
+    private static final BigDecimal DEFAULT_OVERHEAD_COST_RATE = new BigDecimal("0.10");
+
+    private static final BigDecimal SME_RECEIVABLE_RISK_THRESHOLD = new BigDecimal("500000");
+    private static final BigDecimal SME_INVENTORY_RISK_THRESHOLD = new BigDecimal("1000000");
+    private static final BigDecimal SME_PROFIT_WARNING_RATE = new BigDecimal("0.08");
 
     private final SalesLedgerMapper salesLedgerMapper;
     private final SalesLedgerProductMapper salesLedgerProductMapper;
@@ -194,13 +201,17 @@
                                            @P(value = "鍏抽敭璇嶏紝鍙尮閰嶅悎鍚屽彿/瀹㈡埛/椤圭洰", required = false) String keyword,
                                            @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�50", required = false) Integer limit) {
         LoginUser loginUser = currentLoginUser(memoryId);
+        if (loginUser == null) {
+            return jsonResponse(false, "financial_cost_accounting", "鐢ㄦ埛淇℃伅鑾峰彇澶辫触", Map.of(), Map.of(), Map.of());
+        }
+
         DateRange range = resolveDateRange(startDate, endDate, timeRange, "杩�30澶�");
         AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, keyword, limit);
 
         Map<String, Object> summary = new LinkedHashMap<>();
         summary.put("timeRange", range.label());
-        summary.put("startDate", range.start().toString());
-        summary.put("endDate", range.end().toString());
+        summary.put("startDate", displayDate(range.start()));
+        summary.put("endDate", displayDate(range.end()));
         summary.put("orderCount", bundle.orderMetrics().size());
         summary.put("totalRevenue", bundle.totalRevenue());
         summary.put("totalMaterialCost", bundle.totalMaterialCost());
@@ -248,12 +259,16 @@
                                      @P(value = "鍏抽敭璇嶏紝鍙尮閰嶅悎鍚屽彿/瀹㈡埛/椤圭洰", required = false) String keyword,
                                      @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�50", required = false) Integer limit) {
         LoginUser loginUser = currentLoginUser(memoryId);
+        if (loginUser == null) {
+            return jsonResponse(false, "financial_order_profit_analysis", "鐢ㄦ埛淇℃伅鑾峰彇澶辫触", Map.of(), Map.of(), Map.of());
+        }
+
         DateRange range = resolveDateRange(startDate, endDate, timeRange, "杩�30澶�");
         AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, keyword, limit);
         List<OrderProfitMetric> metrics = bundle.orderMetrics();
 
         List<OrderProfitMetric> riskyOrders = metrics.stream()
-                .filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0 || item.profitRate().compareTo(new BigDecimal("0.08")) < 0)
+                .filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0 || item.profitRate().compareTo(SME_PROFIT_WARNING_RATE) < 0)
                 .sorted(Comparator.comparing(OrderProfitMetric::profitRate)
                         .thenComparing(OrderProfitMetric::profit))
                 .toList();
@@ -268,8 +283,8 @@
 
         Map<String, Object> summary = new LinkedHashMap<>();
         summary.put("timeRange", range.label());
-        summary.put("startDate", range.start().toString());
-        summary.put("endDate", range.end().toString());
+        summary.put("startDate", displayDate(range.start()));
+        summary.put("endDate", displayDate(range.end()));
         summary.put("orderCount", metrics.size());
         summary.put("lossOrderCount", metrics.stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count());
         summary.put("lowProfitOrderCount", riskyOrders.size());
@@ -394,8 +409,8 @@
 
         Map<String, Object> summary = new LinkedHashMap<>();
         summary.put("timeRange", range.label());
-        summary.put("startDate", range.start().toString());
-        summary.put("endDate", range.end().toString());
+        summary.put("startDate", displayDate(range.start()));
+        summary.put("endDate", displayDate(range.end()));
         summary.put("actualIncomeTotal", collections.stream().map(AccountSalesCollection::getCollectionAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
         summary.put("actualExpenseTotal", payments.stream().map(AccountPurchasePayment::getPaymentAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
         summary.put("receivableBalance", receivableTotal);
@@ -481,8 +496,8 @@
         List<Map<String, Object>> topAnomalies = anomalyItems.stream().limit(finalLimit).toList();
         Map<String, Object> summary = new LinkedHashMap<>();
         summary.put("timeRange", range.label());
-        summary.put("startDate", range.start().toString());
-        summary.put("endDate", range.end().toString());
+        summary.put("startDate", displayDate(range.start()));
+        summary.put("endDate", displayDate(range.end()));
         summary.put("anomalyCount", topAnomalies.size());
         summary.put("highRiskCount", topAnomalies.stream().filter(item -> "high".equals(item.get("riskLevel"))).count());
         summary.put("mediumRiskCount", topAnomalies.stream().filter(item -> "medium".equals(item.get("riskLevel"))).count());
@@ -521,8 +536,8 @@
 
         Map<String, Object> summary = new LinkedHashMap<>();
         summary.put("timeRange", range.label());
-        summary.put("startDate", range.start().toString());
-        summary.put("endDate", range.end().toString());
+        summary.put("startDate", displayDate(range.start()));
+        summary.put("endDate", displayDate(range.end()));
         summary.put("outputValue", outputValue);
         summary.put("profit", profitBundle.totalProfit());
         summary.put("profitRate", toPercent(profitRate));
@@ -594,18 +609,18 @@
         if (lossCount > 0) {
             riskSuggestions.add(riskSuggestion("鍒╂鼎椋庨櫓", "楂�", "澶嶆牳浜忔崯璁㈠崟BOM鍜屽伐搴忓伐璧勫畾棰濓紝蹇呰鏃惰皟鏁存姤浠蜂笌浜や粯鑺傚銆�"));
         }
-        if (snapshot.receivableTotal().compareTo(new BigDecimal("1000000")) > 0) {
+        if (snapshot.receivableTotal().compareTo(SME_RECEIVABLE_RISK_THRESHOLD) > 0) {
             riskSuggestions.add(riskSuggestion("鍥炴椋庨櫓", "涓�", "瀵瑰簲鏀禩OP瀹㈡埛寤虹珛鍛ㄥ害鍥炴璁″垝锛屽苟璁剧疆棰勮闃堝�笺��"));
         }
-        if (inventoryValue.compareTo(new BigDecimal("3000000")) > 0) {
+        if (inventoryValue.compareTo(SME_INVENTORY_RISK_THRESHOLD) > 0) {
             riskSuggestions.add(riskSuggestion("搴撳瓨椋庨櫓", "涓�", "瀵归珮閲戦鍛嗘粸搴撳瓨鎵ц闄嶄环銆佹浛浠e拰鐢熶骇娑堣�楃瓥鐣ャ��"));
         }
 
         Map<String, Object> summary = new LinkedHashMap<>();
         summary.put("reportType", type);
         summary.put("timeRange", range.label());
-        summary.put("startDate", range.start().toString());
-        summary.put("endDate", range.end().toString());
+        summary.put("startDate", displayDate(range.start()));
+        summary.put("endDate", displayDate(range.end()));
         summary.put("orderCount", bundle.orderMetrics().size());
         summary.put("lossOrderCount", lossCount);
         summary.put("riskSuggestionCount", riskSuggestions.size());
@@ -721,9 +736,11 @@
         applyTenantFilter(outWrapper, loginUser.getTenantId(), ProcurementRecordOut::getTenantId);
         applyDeptFilter(outWrapper, loginUser.getCurrentDeptId(), ProcurementRecordOut::getDeptId);
         outWrapper.eq(ProcurementRecordOut::getType, 2)
-                .in(ProcurementRecordOut::getSalesLedgerProductId, ledgerProductIds)
-                .ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay())
-                .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay());
+                .in(ProcurementRecordOut::getSalesLedgerProductId, ledgerProductIds);
+        if (range.hasDateFilter()) {
+            outWrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay())
+                    .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay());
+        }
         List<ProcurementRecordOut> outList = defaultList(procurementRecordOutMapper.selectList(outWrapper));
 
         Set<Integer> storageIds = outList.stream()
@@ -778,8 +795,10 @@
 
         LambdaQueryWrapper<ProductionOrder> orderWrapper = new LambdaQueryWrapper<>();
         applyDeptFilter(orderWrapper, loginUser.getCurrentDeptId(), ProductionOrder::getDeptId);
-        orderWrapper.ge(ProductionOrder::getCreateTime, range.start().atStartOfDay().minusMonths(2))
-                .lt(ProductionOrder::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1));
+        if (range.hasDateFilter()) {
+            orderWrapper.ge(ProductionOrder::getCreateTime, range.start().atStartOfDay().minusMonths(2))
+                    .lt(ProductionOrder::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1));
+        }
         List<ProductionOrder> orders = defaultList(productionOrderMapper.selectList(orderWrapper));
 
         Map<Long, Set<Long>> orderIdToLedgerIds = new HashMap<>();
@@ -815,9 +834,11 @@
 
         LambdaQueryWrapper<ProductionProductMain> mainWrapper = new LambdaQueryWrapper<>();
         applyDeptFilter(mainWrapper, loginUser.getCurrentDeptId(), ProductionProductMain::getDeptId);
-        mainWrapper.in(ProductionProductMain::getProductionOperationTaskId, taskIdToOrderId.keySet())
-                .ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay().minusMonths(2))
-                .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1));
+        mainWrapper.in(ProductionProductMain::getProductionOperationTaskId, taskIdToOrderId.keySet());
+        if (range.hasDateFilter()) {
+            mainWrapper.ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay().minusMonths(2))
+                    .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1));
+        }
         List<ProductionProductMain> mainList = defaultList(productionProductMainMapper.selectList(mainWrapper));
         Map<Long, Set<Long>> mainIdToLedgers = new HashMap<>();
         for (ProductionProductMain main : mainList) {
@@ -837,9 +858,11 @@
 
         LambdaQueryWrapper<ProductionAccount> accountWrapper = new LambdaQueryWrapper<>();
         applyDeptFilter(accountWrapper, loginUser.getCurrentDeptId(), ProductionAccount::getDeptId);
-        accountWrapper.in(ProductionAccount::getProductionProductMainId, mainIdToLedgers.keySet())
-                .ge(ProductionAccount::getSchedulingDate, range.start().atStartOfDay())
-                .lt(ProductionAccount::getSchedulingDate, range.end().plusDays(1).atStartOfDay());
+        accountWrapper.in(ProductionAccount::getProductionProductMainId, mainIdToLedgers.keySet());
+        if (range.hasDateFilter()) {
+            accountWrapper.ge(ProductionAccount::getSchedulingDate, range.start().atStartOfDay())
+                    .lt(ProductionAccount::getSchedulingDate, range.end().plusDays(1).atStartOfDay());
+        }
         List<ProductionAccount> accountList = defaultList(productionAccountMapper.selectList(accountWrapper));
 
         Map<String, BigDecimal> salaryQuotaByOperation = defaultList(technologyOperationMapper.selectList(new LambdaQueryWrapper<TechnologyOperation>()
@@ -868,9 +891,11 @@
 
         LambdaQueryWrapper<ProductionProductOutput> outputWrapper = new LambdaQueryWrapper<>();
         applyDeptFilter(outputWrapper, loginUser.getCurrentDeptId(), ProductionProductOutput::getDeptId);
-        outputWrapper.in(ProductionProductOutput::getProductionProductMainId, mainIdToLedgers.keySet())
-                .ge(ProductionProductOutput::getCreateTime, range.start().atStartOfDay())
-                .lt(ProductionProductOutput::getCreateTime, range.end().plusDays(1).atStartOfDay());
+        outputWrapper.in(ProductionProductOutput::getProductionProductMainId, mainIdToLedgers.keySet());
+        if (range.hasDateFilter()) {
+            outputWrapper.ge(ProductionProductOutput::getCreateTime, range.start().atStartOfDay())
+                    .lt(ProductionProductOutput::getCreateTime, range.end().plusDays(1).atStartOfDay());
+        }
         List<ProductionProductOutput> outputList = defaultList(productionProductOutputMapper.selectList(outputWrapper));
         Map<Long, BigDecimal> scrapCostByLedger = new HashMap<>();
         for (ProductionProductOutput output : outputList) {
@@ -909,10 +934,14 @@
                     .or().like(SalesLedger::getProjectName, keyword)
                     .or().like(SalesLedger::getSalesman, keyword));
         }
-        wrapper.ge(SalesLedger::getEntryDate, toDate(range.start()))
-                .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end()))
-                .orderByDesc(SalesLedger::getEntryDate, SalesLedger::getId)
-                .last("limit " + normalizeLimit(limit));
+        if (range.hasDateFilter()) {
+            wrapper.ge(SalesLedger::getEntryDate, toDate(range.start()))
+                    .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end()));
+        }
+        wrapper.orderByDesc(SalesLedger::getEntryDate, SalesLedger::getId);
+        if (limit != null && limit > 0) {
+            wrapper.last("limit " + normalizeLimit(limit));
+        }
         return defaultList(salesLedgerMapper.selectList(wrapper));
     }
 
@@ -1002,8 +1031,10 @@
         if (productModelIds != null && !productModelIds.isEmpty()) {
             wrapper.in(ProcurementRecordOut::getProductModelId, productModelIds);
         }
-        wrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay())
-                .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay());
+        if (range.hasDateFilter()) {
+            wrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay())
+                    .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay());
+        }
         List<ProcurementRecordOut> outList = defaultList(procurementRecordOutMapper.selectList(wrapper));
         if (outList.isEmpty()) {
             return OutboundStats.empty();
@@ -1098,18 +1129,22 @@
     private List<AccountSalesCollection> queryCollections(LoginUser loginUser, DateRange range) {
         LambdaQueryWrapper<AccountSalesCollection> wrapper = new LambdaQueryWrapper<>();
         applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountSalesCollection::getDeptId);
-        wrapper.ge(AccountSalesCollection::getCollectionDate, range.start())
-                .le(AccountSalesCollection::getCollectionDate, range.end())
-                .orderByAsc(AccountSalesCollection::getCollectionDate);
+        if (range.hasDateFilter()) {
+            wrapper.ge(AccountSalesCollection::getCollectionDate, range.start())
+                    .le(AccountSalesCollection::getCollectionDate, range.end());
+        }
+        wrapper.orderByAsc(AccountSalesCollection::getCollectionDate);
         return defaultList(accountSalesCollectionMapper.selectList(wrapper));
     }
 
     private List<AccountPurchasePayment> queryPayments(LoginUser loginUser, DateRange range) {
         LambdaQueryWrapper<AccountPurchasePayment> wrapper = new LambdaQueryWrapper<>();
         applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountPurchasePayment::getDeptId);
-        wrapper.ge(AccountPurchasePayment::getPaymentDate, range.start())
-                .le(AccountPurchasePayment::getPaymentDate, range.end())
-                .orderByAsc(AccountPurchasePayment::getPaymentDate);
+        if (range.hasDateFilter()) {
+            wrapper.ge(AccountPurchasePayment::getPaymentDate, range.start())
+                    .le(AccountPurchasePayment::getPaymentDate, range.end());
+        }
+        wrapper.orderByAsc(AccountPurchasePayment::getPaymentDate);
         return defaultList(accountPurchasePaymentMapper.selectList(wrapper));
     }
 
@@ -1118,8 +1153,12 @@
                                                        List<AccountPurchasePayment> payments) {
         Map<YearMonth, BigDecimal> incomeByMonth = new LinkedHashMap<>();
         Map<YearMonth, BigDecimal> expenseByMonth = new LinkedHashMap<>();
-        YearMonth startMonth = YearMonth.from(range.start());
-        YearMonth endMonth = YearMonth.from(range.end());
+        DateRange monthlyRange = range.hasDateFilter() ? range : inferCashFlowRange(collections, payments);
+        if (!monthlyRange.hasDateFilter()) {
+            return List.of();
+        }
+        YearMonth startMonth = YearMonth.from(monthlyRange.start());
+        YearMonth endMonth = YearMonth.from(monthlyRange.end());
         for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) {
             incomeByMonth.put(month, BigDecimal.ZERO);
             expenseByMonth.put(month, BigDecimal.ZERO);
@@ -1151,6 +1190,27 @@
             result.add(new MonthlyCashFlow(month.toString(), income, expense, income.subtract(expense)));
         }
         return result;
+    }
+
+    private DateRange inferCashFlowRange(List<AccountSalesCollection> collections,
+                                         List<AccountPurchasePayment> payments) {
+        LocalDate min = null;
+        LocalDate max = null;
+        for (AccountSalesCollection row : defaultList(collections)) {
+            if (row.getCollectionDate() == null) {
+                continue;
+            }
+            min = min == null || row.getCollectionDate().isBefore(min) ? row.getCollectionDate() : min;
+            max = max == null || row.getCollectionDate().isAfter(max) ? row.getCollectionDate() : max;
+        }
+        for (AccountPurchasePayment row : defaultList(payments)) {
+            if (row.getPaymentDate() == null) {
+                continue;
+            }
+            min = min == null || row.getPaymentDate().isBefore(min) ? row.getPaymentDate() : min;
+            max = max == null || row.getPaymentDate().isAfter(max) ? row.getPaymentDate() : max;
+        }
+        return min == null || max == null ? new DateRange(null, null, "鍏ㄩ儴") : new DateRange(min, max, "鍏ㄩ儴");
     }
 
     private List<MonthlyCashFlow> forecastMonthlyCashFlow(List<MonthlyCashFlow> actual, int forecastMonths) {
@@ -1274,7 +1334,35 @@
                 return productAmount;
             }
         }
-        return revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE);
+
+        BigDecimal materialCost = revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE);
+        BigDecimal laborCost = revenue.multiply(DEFAULT_LABOR_COST_RATE);
+        BigDecimal overheadCost = revenue.multiply(DEFAULT_OVERHEAD_COST_RATE);
+
+        return materialCost.add(laborCost).add(overheadCost);
+    }
+
+    private BigDecimal estimateTotalCost(BigDecimal revenue, List<SalesLedgerProduct> products) {
+        if (revenue == null || revenue.compareTo(BigDecimal.ZERO) <= 0) {
+            return BigDecimal.ZERO;
+        }
+
+        BigDecimal materialCost = BigDecimal.ZERO;
+        if (products != null && !products.isEmpty()) {
+            materialCost = products.stream()
+                    .map(SalesLedgerProduct::getTaxExclusiveTotalPrice)
+                    .filter(Objects::nonNull)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+        }
+
+        if (materialCost.compareTo(BigDecimal.ZERO) <= 0) {
+            materialCost = revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE);
+        }
+
+        BigDecimal laborCost = revenue.multiply(DEFAULT_LABOR_COST_RATE);
+        BigDecimal overheadCost = revenue.multiply(DEFAULT_OVERHEAD_COST_RATE);
+
+        return materialCost.add(laborCost).add(overheadCost);
     }
 
     private Map<String, String> queryCustomerNameMap(Set<String> idSet) {
@@ -1352,6 +1440,9 @@
     }
 
     private DateRange previousSameLengthRange(DateRange range) {
+        if (!range.hasDateFilter()) {
+            return new DateRange(null, null, "鍏ㄩ儴");
+        }
         long days = daysBetween(range.start(), range.end()) + 1L;
         LocalDate prevEnd = range.start().minusDays(1);
         LocalDate prevStart = prevEnd.minusDays(days - 1L);
@@ -1770,20 +1861,7 @@
         }
 
         if (!StringUtils.hasText(timeRange)) {
-            if ("浠婂ぉ".equals(defaultLabel)) {
-                return new DateRange(today, today, "浠婂ぉ");
-            }
-            if ("鏈懆".equals(defaultLabel)) {
-                LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
-                return new DateRange(start, today, "鏈懆");
-            }
-            if ("鏈湀".equals(defaultLabel)) {
-                return new DateRange(today.withDayOfMonth(1), today, "鏈湀");
-            }
-            if ("杩�90澶�".equals(defaultLabel)) {
-                return new DateRange(today.minusDays(89), today, "杩�90澶�");
-            }
-            return new DateRange(today.minusDays(29), today, defaultLabel);
+            return new DateRange(null, null, "鍏ㄩ儴");
         }
 
         String text = timeRange.trim();
@@ -1840,7 +1918,7 @@
                 return new DateRange(start, end, start + "鑷�" + end);
             }
         }
-        return new DateRange(today.minusDays(29), today, "杩�30澶�");
+        return new DateRange(null, null, "鍏ㄩ儴");
     }
 
     private LocalDate parseLocalDate(String text) {
@@ -1870,6 +1948,10 @@
     }
 
     private String formatDate(LocalDate date) {
+        return date == null ? "" : date.format(DATE_FMT);
+    }
+
+    private String displayDate(LocalDate date) {
         return date == null ? "" : date.format(DATE_FMT);
     }
 
@@ -1996,8 +2078,8 @@
     private Map<String, Object> rangeSummary(DateRange range, int count, String keyword) {
         Map<String, Object> summary = new LinkedHashMap<>();
         summary.put("timeRange", range.label());
-        summary.put("startDate", range.start().toString());
-        summary.put("endDate", range.end().toString());
+        summary.put("startDate", displayDate(range.start()));
+        summary.put("endDate", displayDate(range.end()));
         summary.put("count", count);
         summary.put("keyword", safe(keyword));
         return summary;
@@ -2080,6 +2162,9 @@
     }
 
     private record DateRange(LocalDate start, LocalDate end, String label) {
+        private boolean hasDateFilter() {
+            return start != null && end != null;
+        }
     }
 
     private record OrderProfitMetric(Long ledgerId,

--
Gitblit v1.9.3