Erweiterungen
This commit is contained in:
@@ -2,6 +2,7 @@ package de.assecutor.votianlt.ai.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import de.assecutor.votianlt.model.Job;
|
||||
import de.assecutor.votianlt.model.JobStatus;
|
||||
import de.assecutor.votianlt.service.JobStatisticsService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -47,40 +48,32 @@ public class AiStatisticsService {
|
||||
public StatisticsResponse analyzeStatisticsQuery(String userQuery) {
|
||||
log.info("Processing statistics query: {}", userQuery);
|
||||
|
||||
// Gather current statistics
|
||||
String statisticsContext = buildStatisticsContext();
|
||||
|
||||
// Determine query type and prepare chart data
|
||||
// Determine query type and prepare chart data (includes customer filter detection)
|
||||
QueryAnalysis analysis = analyzeQueryType(userQuery);
|
||||
log.debug("Query analysis - Type: {}, Chart: {}", analysis.queryType, analysis.chartType);
|
||||
log.debug("Query analysis - Type: {}, Chart: {}, Customer: {}, Status: {}",
|
||||
analysis.queryType, analysis.chartType,
|
||||
analysis.customerFilter != null ? analysis.customerFilter : "none",
|
||||
analysis.statusFilter != null ? analysis.statusFilter : "none");
|
||||
|
||||
// Gather context (statistics or job list depending on query type)
|
||||
String statisticsContext = buildStatisticsContext(analysis);
|
||||
|
||||
// Build prompt for LLM
|
||||
String prompt = buildPrompt(userQuery, statisticsContext, analysis);
|
||||
|
||||
// System prompt for the statistics assistant
|
||||
String systemPrompt = """
|
||||
Du bist ein hilfreicher Statistik-Assistent für ein Logistikunternehmen.
|
||||
Beantworte die Frage des Benutzers basierend auf den aktuellen Statistiken.
|
||||
|
||||
WICHTIGE FORMATIERUNGSREGELN:
|
||||
- Verwende KEINE Tabellen (keine | oder --- Zeichen)
|
||||
- Die Daten werden bereits als interaktives Diagramm visualisiert
|
||||
- Fasse die wichtigsten Erkenntnisse in Fließtext oder kurzen Aufzählungen zusammen
|
||||
- Nenne konkrete Zahlen im Text, aber liste nicht alle Werte tabellarisch auf
|
||||
|
||||
Antworte auf Deutsch, präzise und freundlich.
|
||||
Erkläre die Daten kurz und gib bei Bedarf Empfehlungen.
|
||||
Halte die Antwort kompakt (max. 3-4 Sätze für einfache Fragen, mehr für komplexe).
|
||||
""";
|
||||
// System prompt - different for list vs statistics queries
|
||||
String systemPrompt = analysis.queryType.equals("list")
|
||||
? buildListSystemPrompt()
|
||||
: buildStatisticsSystemPrompt();
|
||||
|
||||
// Call LLM via direct REST client (like aimailassistant)
|
||||
String llmResponse = llmClient.chat(systemPrompt, prompt);
|
||||
|
||||
if (llmResponse != null) {
|
||||
if (llmResponse != null && !llmResponse.isBlank()) {
|
||||
log.info("LLM response received, length: {} chars", llmResponse.length());
|
||||
return new StatisticsResponse(llmResponse, analysis.chartType, analysis.chartData);
|
||||
} else {
|
||||
log.warn("LLM returned null response, using fallback");
|
||||
log.warn("LLM returned null or blank response, using fallback");
|
||||
return new StatisticsResponse(
|
||||
buildFallbackResponse(analysis),
|
||||
analysis.chartType,
|
||||
@@ -92,45 +85,284 @@ public class AiStatisticsService {
|
||||
private record QueryAnalysis(
|
||||
String queryType,
|
||||
String chartType,
|
||||
String chartData
|
||||
String chartData,
|
||||
String customerFilter, // null = no filter, show all data
|
||||
JobStatus statusFilter // null = no status filter
|
||||
) {}
|
||||
|
||||
private String buildStatisticsSystemPrompt() {
|
||||
return """
|
||||
Du bist ein Statistik-Assistent für ein Logistikunternehmen.
|
||||
|
||||
WICHTIG - ANTWORTSTIL:
|
||||
- Beantworte NUR die gestellte Frage - keine zusätzlichen Informationen!
|
||||
- Keine allgemeinen Tipps, Empfehlungen oder weiterführende Hinweise
|
||||
- Keine Vergleiche mit anderen Daten, außer explizit gefragt
|
||||
- Kurz und präzise: maximal 2-3 Sätze
|
||||
- Nenne die relevanten Zahlen direkt
|
||||
|
||||
DIAGRAMM-HINWEIS:
|
||||
- Ein Diagramm wird AUTOMATISCH angezeigt - nicht erwähnen oder beschreiben
|
||||
- Sage NIEMALS "Ich kann kein Diagramm zeichnen" oder ähnliches
|
||||
|
||||
FORMATIERUNG:
|
||||
- Keine Tabellen
|
||||
- Keine langen Aufzählungen
|
||||
- Fließtext bevorzugen
|
||||
|
||||
Antworte auf Deutsch.
|
||||
""";
|
||||
}
|
||||
|
||||
private String buildListSystemPrompt() {
|
||||
return """
|
||||
Du bist ein Assistent für ein Logistikunternehmen.
|
||||
|
||||
WICHTIG - ANTWORTSTIL:
|
||||
- Der Benutzer fragt nach einer Liste von Jobs/Aufträgen
|
||||
- Die Job-Liste wird bereits als Daten angezeigt
|
||||
- Fasse nur kurz zusammen, was gefunden wurde (z.B. "Es wurden X Jobs gefunden")
|
||||
- Keine detaillierte Auflistung der Jobs nötig - die Daten sind bereits sichtbar
|
||||
- Maximal 1-2 Sätze
|
||||
|
||||
KEIN DIAGRAMM:
|
||||
- Es wird KEIN Diagramm angezeigt bei Listen-Anfragen
|
||||
- Erwähne keine Diagramme
|
||||
|
||||
Antworte auf Deutsch.
|
||||
""";
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect user-specified chart type from the query.
|
||||
* Returns null if no specific chart type was requested.
|
||||
*/
|
||||
private String detectUserChartTypePreference(String query) {
|
||||
String lowerQuery = query.toLowerCase();
|
||||
|
||||
// Balkendiagramm / Bar Chart
|
||||
if (lowerQuery.contains("balken") || lowerQuery.contains("bar chart") ||
|
||||
lowerQuery.contains("säulen") || lowerQuery.contains("balkendiagramm")) {
|
||||
return "bar";
|
||||
}
|
||||
|
||||
// Tortendiagramm / Pie Chart
|
||||
if (lowerQuery.contains("torten") || lowerQuery.contains("pie") ||
|
||||
lowerQuery.contains("kreis") || lowerQuery.contains("tortendiagramm") ||
|
||||
lowerQuery.contains("kreisdiagramm")) {
|
||||
return "pie";
|
||||
}
|
||||
|
||||
// Donut / Ring Chart
|
||||
if (lowerQuery.contains("donut") || lowerQuery.contains("ring") ||
|
||||
lowerQuery.contains("doughnut")) {
|
||||
return "doughnut";
|
||||
}
|
||||
|
||||
// Liniendiagramm / Line Chart
|
||||
if (lowerQuery.contains("linie") || lowerQuery.contains("line") ||
|
||||
lowerQuery.contains("liniendiagramm") || lowerQuery.contains("kurve") ||
|
||||
lowerQuery.contains("graph")) {
|
||||
return "line";
|
||||
}
|
||||
|
||||
// Flächendiagramm / Area Chart
|
||||
if (lowerQuery.contains("fläche") || lowerQuery.contains("area") ||
|
||||
lowerQuery.contains("flächendiagramm")) {
|
||||
return "line"; // Line with fill=true
|
||||
}
|
||||
|
||||
// Radar Chart
|
||||
if (lowerQuery.contains("radar") || lowerQuery.contains("netz") ||
|
||||
lowerQuery.contains("spinne")) {
|
||||
return "radar";
|
||||
}
|
||||
|
||||
// Polararea Chart
|
||||
if (lowerQuery.contains("polar")) {
|
||||
return "polarArea";
|
||||
}
|
||||
|
||||
return null; // No specific preference
|
||||
}
|
||||
|
||||
private QueryAnalysis analyzeQueryType(String query) {
|
||||
String lowerQuery = query.toLowerCase();
|
||||
|
||||
// Status-bezogene Anfragen
|
||||
if (lowerQuery.contains("status") || lowerQuery.contains("offen") ||
|
||||
lowerQuery.contains("abgeschlossen") || lowerQuery.contains("zählen") ||
|
||||
lowerQuery.contains("anzahl") || lowerQuery.contains("wie viele")) {
|
||||
return new QueryAnalysis("status", "doughnut", buildStatusChartData());
|
||||
// First, check if this is a LIST query (no chart needed)
|
||||
boolean isListQuery = isListQuery(lowerQuery);
|
||||
log.debug("Is list query: {}", isListQuery);
|
||||
|
||||
// Check if user specified a chart type (only relevant for non-list queries)
|
||||
String userChartType = isListQuery ? null : detectUserChartTypePreference(query);
|
||||
log.debug("User chart type preference: {}", userChartType != null ? userChartType : "none");
|
||||
|
||||
// Check if user specified a customer filter
|
||||
String customerFilter = detectCustomerFilter(query);
|
||||
log.debug("Customer filter: {}", customerFilter != null ? customerFilter : "none (showing all data)");
|
||||
|
||||
// Check if user specified a status filter
|
||||
JobStatus statusFilter = detectStatusFilter(lowerQuery);
|
||||
log.debug("Status filter: {}", statusFilter != null ? statusFilter : "none");
|
||||
|
||||
// For list queries, return no chart
|
||||
if (isListQuery) {
|
||||
return new QueryAnalysis("list", null, null, customerFilter, statusFilter);
|
||||
}
|
||||
|
||||
// Determine query type and default chart type
|
||||
String queryType;
|
||||
String defaultChartType;
|
||||
String chartData;
|
||||
|
||||
// Status-bezogene Anfragen (Statistik, nicht Liste)
|
||||
if ((lowerQuery.contains("status") && (lowerQuery.contains("statistik") || lowerQuery.contains("verteilung") ||
|
||||
lowerQuery.contains("übersicht") || lowerQuery.contains("wie viele"))) ||
|
||||
lowerQuery.contains("zählen") || lowerQuery.contains("anzahl")) {
|
||||
queryType = "status";
|
||||
defaultChartType = "doughnut";
|
||||
chartData = buildStatusChartData(customerFilter);
|
||||
}
|
||||
// Umsatz-bezogene Anfragen
|
||||
if (lowerQuery.contains("umsatz") || lowerQuery.contains("revenue") ||
|
||||
lowerQuery.contains("kunde") || lowerQuery.contains("customer") ||
|
||||
else if (lowerQuery.contains("umsatz") || lowerQuery.contains("revenue") ||
|
||||
lowerQuery.contains("einnahmen")) {
|
||||
return new QueryAnalysis("revenue", "bar", buildRevenueChartData());
|
||||
queryType = "revenue";
|
||||
defaultChartType = "bar";
|
||||
chartData = customerFilter != null ? buildCustomerRevenueChartData(customerFilter) : buildRevenueChartData();
|
||||
}
|
||||
|
||||
// Trend-bezogene Anfragen
|
||||
if (lowerQuery.contains("trend") || lowerQuery.contains("monat") ||
|
||||
else if (lowerQuery.contains("trend") || lowerQuery.contains("monat") ||
|
||||
lowerQuery.contains("entwicklung") || lowerQuery.contains("jahr") ||
|
||||
lowerQuery.contains("verlauf")) {
|
||||
return new QueryAnalysis("trend", "line", buildTrendChartData());
|
||||
queryType = "trend";
|
||||
defaultChartType = "line";
|
||||
chartData = buildTrendChartData(customerFilter);
|
||||
}
|
||||
|
||||
// Task-bezogene Anfragen
|
||||
if (lowerQuery.contains("task") || lowerQuery.contains("aufgabe") ||
|
||||
else if (lowerQuery.contains("task") || lowerQuery.contains("aufgabe") ||
|
||||
lowerQuery.contains("erledigt")) {
|
||||
return new QueryAnalysis("tasks", "doughnut", buildTaskChartData());
|
||||
queryType = "tasks";
|
||||
defaultChartType = "doughnut";
|
||||
chartData = buildTaskChartData();
|
||||
}
|
||||
// Allgemeine Übersicht
|
||||
else {
|
||||
queryType = "overview";
|
||||
defaultChartType = "bar";
|
||||
chartData = buildOverviewChartData(customerFilter);
|
||||
}
|
||||
|
||||
// Allgemeine Übersicht
|
||||
return new QueryAnalysis("overview", "bar", buildOverviewChartData());
|
||||
// Use user's chart type preference if specified, otherwise use default
|
||||
String chartType = userChartType != null ? userChartType : defaultChartType;
|
||||
|
||||
return new QueryAnalysis(queryType, chartType, chartData, customerFilter, null);
|
||||
}
|
||||
|
||||
private String buildStatusChartData() {
|
||||
Map<JobStatus, Long> statusCounts = statisticsService.getJobCountsByStatus();
|
||||
/**
|
||||
* Detect status filter from the query.
|
||||
*/
|
||||
private JobStatus detectStatusFilter(String lowerQuery) {
|
||||
if (lowerQuery.contains("angelegt") || lowerQuery.contains("erstellt") || lowerQuery.contains("created")) {
|
||||
return JobStatus.CREATED;
|
||||
}
|
||||
if (lowerQuery.contains("in bearbeitung") || lowerQuery.contains("in progress")) {
|
||||
return JobStatus.IN_PROGRESS;
|
||||
}
|
||||
if (lowerQuery.contains("abholung geplant") || lowerQuery.contains("pickup scheduled")) {
|
||||
return JobStatus.PICKUP_SCHEDULED;
|
||||
}
|
||||
if (lowerQuery.contains("abgeholt") || lowerQuery.contains("picked up")) {
|
||||
return JobStatus.PICKED_UP;
|
||||
}
|
||||
if (lowerQuery.contains("in transport") || lowerQuery.contains("in transit") || lowerQuery.contains("unterwegs")) {
|
||||
return JobStatus.IN_TRANSIT;
|
||||
}
|
||||
if (lowerQuery.contains("zugestellt") || lowerQuery.contains("delivered") || lowerQuery.contains("geliefert")) {
|
||||
return JobStatus.DELIVERED;
|
||||
}
|
||||
if (lowerQuery.contains("abgeschlossen") || lowerQuery.contains("completed") || lowerQuery.contains("fertig")) {
|
||||
return JobStatus.COMPLETED;
|
||||
}
|
||||
if (lowerQuery.contains("storniert") || lowerQuery.contains("cancelled") || lowerQuery.contains("abgebrochen")) {
|
||||
return JobStatus.CANCELLED;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the query is asking for a list of jobs (not statistics).
|
||||
*/
|
||||
private boolean isListQuery(String lowerQuery) {
|
||||
// Keywords that indicate a list/detail query (not statistics)
|
||||
boolean hasListKeywords = lowerQuery.contains("zeige alle") ||
|
||||
lowerQuery.contains("liste") ||
|
||||
lowerQuery.contains("welche jobs") ||
|
||||
lowerQuery.contains("alle jobs") ||
|
||||
lowerQuery.contains("alle aufträge") ||
|
||||
lowerQuery.contains("zeige die jobs") ||
|
||||
lowerQuery.contains("zeige die aufträge") ||
|
||||
lowerQuery.contains("zeig mir die") ||
|
||||
lowerQuery.contains("gib mir die");
|
||||
|
||||
// Keywords that indicate statistics (override list detection)
|
||||
boolean hasStatsKeywords = lowerQuery.contains("statistik") ||
|
||||
lowerQuery.contains("diagramm") ||
|
||||
lowerQuery.contains("chart") ||
|
||||
lowerQuery.contains("verteilung") ||
|
||||
lowerQuery.contains("wie viele") ||
|
||||
lowerQuery.contains("anzahl") ||
|
||||
lowerQuery.contains("zähle") ||
|
||||
lowerQuery.contains("umsatz") ||
|
||||
lowerQuery.contains("trend") ||
|
||||
lowerQuery.contains("torte") ||
|
||||
lowerQuery.contains("balken");
|
||||
|
||||
return hasListKeywords && !hasStatsKeywords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect customer filter from the query.
|
||||
* Returns the matching customer name or null if no filter detected.
|
||||
*/
|
||||
private String detectCustomerFilter(String query) {
|
||||
String lowerQuery = query.toLowerCase();
|
||||
|
||||
// Keywords that indicate a customer filter
|
||||
String[] filterIndicators = {
|
||||
"für ", "von ", "bei ", "kunde ", "firma ", "unternehmen ",
|
||||
"für die firma ", "für den kunden ", "von der firma ", "vom kunden ",
|
||||
"nur ", "ausschließlich ", "speziell "
|
||||
};
|
||||
|
||||
// Check if any indicator is present
|
||||
boolean hasIndicator = false;
|
||||
String foundIndicator = null;
|
||||
for (String indicator : filterIndicators) {
|
||||
int idx = lowerQuery.indexOf(indicator);
|
||||
if (idx >= 0) {
|
||||
hasIndicator = true;
|
||||
foundIndicator = indicator;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasIndicator) {
|
||||
log.debug("detectCustomerFilter - No filter indicator found in: '{}'", query);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug("detectCustomerFilter - Found indicator '{}' in query: '{}'", foundIndicator, query);
|
||||
|
||||
// Try to find a matching customer in the query
|
||||
String matchedCustomer = statisticsService.findMatchingCustomer(query);
|
||||
log.debug("detectCustomerFilter - Matched customer: '{}'", matchedCustomer);
|
||||
return matchedCustomer;
|
||||
}
|
||||
|
||||
private String buildStatusChartData(String customerFilter) {
|
||||
Map<JobStatus, Long> statusCounts = customerFilter != null
|
||||
? statisticsService.getJobCountsByStatusForCustomer(customerFilter)
|
||||
: statisticsService.getJobCountsByStatus();
|
||||
|
||||
List<String> labels = new ArrayList<>();
|
||||
List<Long> data = new ArrayList<>();
|
||||
@@ -154,7 +386,8 @@ public class AiStatisticsService {
|
||||
}
|
||||
}
|
||||
|
||||
return buildChartJson(labels, data, colors.subList(0, Math.min(labels.size(), colors.size())), "Aufträge");
|
||||
String label = customerFilter != null ? "Aufträge (" + customerFilter + ")" : "Aufträge";
|
||||
return buildChartJson(labels, data, colors.subList(0, Math.min(labels.size(), colors.size())), label);
|
||||
}
|
||||
|
||||
private String buildRevenueChartData() {
|
||||
@@ -177,9 +410,30 @@ public class AiStatisticsService {
|
||||
return buildChartJsonDouble(labels, data, colors.subList(0, Math.min(labels.size(), colors.size())), "Umsatz (EUR)");
|
||||
}
|
||||
|
||||
private String buildTrendChartData() {
|
||||
private String buildCustomerRevenueChartData(String customer) {
|
||||
var statusCounts = statisticsService.getJobCountsByStatusForCustomer(customer);
|
||||
var totalRevenue = statisticsService.getTotalRevenueForCustomer(customer);
|
||||
long totalJobs = statisticsService.getTotalJobCountForCustomer(customer);
|
||||
long completed = statusCounts.getOrDefault(JobStatus.COMPLETED, 0L);
|
||||
long inProgress = statusCounts.getOrDefault(JobStatus.IN_PROGRESS, 0L);
|
||||
|
||||
List<String> labels = List.of("Aufträge gesamt", "Abgeschlossen", "In Bearbeitung", "Umsatz (€/100)");
|
||||
List<Double> data = List.of(
|
||||
(double) totalJobs,
|
||||
(double) completed,
|
||||
(double) inProgress,
|
||||
totalRevenue.doubleValue() / 100 // Scale down for better visualization
|
||||
);
|
||||
List<String> colors = List.of("#3b82f6", "#22c55e", "#f59e0b", "#6366f1");
|
||||
|
||||
return buildChartJsonDouble(labels, data, colors, customer);
|
||||
}
|
||||
|
||||
private String buildTrendChartData(String customerFilter) {
|
||||
int currentYear = Year.now().getValue();
|
||||
Map<Month, Long> monthlyData = statisticsService.getMonthlyJobCounts(currentYear);
|
||||
Map<Month, Long> monthlyData = customerFilter != null
|
||||
? statisticsService.getMonthlyJobCountsForCustomer(currentYear, customerFilter)
|
||||
: statisticsService.getMonthlyJobCounts(currentYear);
|
||||
|
||||
List<String> labels = List.of("Jan", "Feb", "Mär", "Apr", "Mai", "Jun",
|
||||
"Jul", "Aug", "Sep", "Okt", "Nov", "Dez");
|
||||
@@ -189,11 +443,15 @@ public class AiStatisticsService {
|
||||
data.add(monthlyData.getOrDefault(month, 0L));
|
||||
}
|
||||
|
||||
String datasetLabel = customerFilter != null
|
||||
? String.format("%s - %d", customerFilter, currentYear)
|
||||
: String.format("Aufträge %d", currentYear);
|
||||
|
||||
return String.format("""
|
||||
{
|
||||
"labels": %s,
|
||||
"datasets": [{
|
||||
"label": "Aufträge %d",
|
||||
"label": "%s",
|
||||
"data": %s,
|
||||
"borderColor": "#6366f1",
|
||||
"backgroundColor": "rgba(99, 102, 241, 0.15)",
|
||||
@@ -206,7 +464,7 @@ public class AiStatisticsService {
|
||||
"fill": true
|
||||
}]
|
||||
}
|
||||
""", toJsonArray(labels), currentYear, data);
|
||||
""", toJsonArray(labels), datasetLabel, data);
|
||||
}
|
||||
|
||||
private String buildTaskChartData() {
|
||||
@@ -222,10 +480,14 @@ public class AiStatisticsService {
|
||||
return buildChartJson(labels, data, colors, "Aufgaben");
|
||||
}
|
||||
|
||||
private String buildOverviewChartData() {
|
||||
Map<JobStatus, Long> statusCounts = statisticsService.getJobCountsByStatus();
|
||||
private String buildOverviewChartData(String customerFilter) {
|
||||
Map<JobStatus, Long> statusCounts = customerFilter != null
|
||||
? statisticsService.getJobCountsByStatusForCustomer(customerFilter)
|
||||
: statisticsService.getJobCountsByStatus();
|
||||
|
||||
long total = statisticsService.getTotalJobCount();
|
||||
long total = customerFilter != null
|
||||
? statisticsService.getTotalJobCountForCustomer(customerFilter)
|
||||
: statisticsService.getTotalJobCount();
|
||||
long completed = statusCounts.getOrDefault(JobStatus.COMPLETED, 0L);
|
||||
long inProgress = statusCounts.getOrDefault(JobStatus.IN_PROGRESS, 0L);
|
||||
long open = total - completed - statusCounts.getOrDefault(JobStatus.CANCELLED, 0L);
|
||||
@@ -234,7 +496,8 @@ public class AiStatisticsService {
|
||||
List<Long> data = List.of(total, completed, inProgress, open);
|
||||
List<String> colors = List.of("#3b82f6", "#22c55e", "#f59e0b", "#06b6d4");
|
||||
|
||||
return buildChartJson(labels, data, colors, "Aufträge");
|
||||
String label = customerFilter != null ? customerFilter : "Aufträge";
|
||||
return buildChartJson(labels, data, colors, label);
|
||||
}
|
||||
|
||||
private String buildChartJson(List<String> labels, List<Long> data, List<String> colors, String label) {
|
||||
@@ -273,37 +536,103 @@ public class AiStatisticsService {
|
||||
}
|
||||
}
|
||||
|
||||
private String buildStatisticsContext() {
|
||||
private String buildStatisticsContext(QueryAnalysis analysis) {
|
||||
StringBuilder context = new StringBuilder();
|
||||
String customerFilter = analysis.customerFilter;
|
||||
JobStatus statusFilter = analysis.statusFilter;
|
||||
|
||||
// For LIST queries, show actual job data
|
||||
if ("list".equals(analysis.queryType)) {
|
||||
return buildListContext(customerFilter, statusFilter);
|
||||
}
|
||||
|
||||
// For statistics queries
|
||||
if (customerFilter != null) {
|
||||
// Filtered statistics for a specific customer
|
||||
context.append(String.format("**Statistiken für Kunde: %s**\n\n", customerFilter));
|
||||
|
||||
var statusCounts = statisticsService.getJobCountsByStatusForCustomer(customerFilter);
|
||||
context.append("**Aufträge nach Status:**\n");
|
||||
statusCounts.forEach((status, count) -> {
|
||||
if (count > 0) {
|
||||
context.append(String.format("- %s: %d Aufträge\n", status.getDisplayName(), count));
|
||||
}
|
||||
});
|
||||
|
||||
context.append(String.format("\n**Übersicht für %s:**\n", customerFilter));
|
||||
context.append(String.format("- Gesamtanzahl Aufträge: %d\n",
|
||||
statisticsService.getTotalJobCountForCustomer(customerFilter)));
|
||||
context.append(String.format("- Abschlussrate: %.1f%%\n",
|
||||
statisticsService.getCompletionRateForCustomer(customerFilter)));
|
||||
context.append(String.format("- Umsatz: %.2f EUR\n",
|
||||
statisticsService.getTotalRevenueForCustomer(customerFilter)));
|
||||
} else {
|
||||
// General statistics (all data)
|
||||
var statusCounts = statisticsService.getJobCountsByStatus();
|
||||
context.append("**Aktuelle Auftragsstatistiken:**\n");
|
||||
statusCounts.forEach((status, count) ->
|
||||
context.append(String.format("- %s: %d Aufträge\n", status.getDisplayName(), count)));
|
||||
|
||||
context.append(String.format("\n**Gesamtübersicht:**\n"));
|
||||
context.append(String.format("- Gesamtanzahl Aufträge: %d\n", statisticsService.getTotalJobCount()));
|
||||
context.append(String.format("- Abschlussrate: %.1f%%\n", statisticsService.getCompletionRate()));
|
||||
context.append(String.format("- Gesamtumsatz: %.2f EUR\n", statisticsService.getTotalRevenue()));
|
||||
|
||||
// Task statistics
|
||||
var taskStats = statisticsService.getTaskCompletionStats();
|
||||
context.append(String.format("\n**Aufgaben:**\n"));
|
||||
context.append(String.format("- Gesamt: %d\n", taskStats.get("total")));
|
||||
context.append(String.format("- Erledigt: %d\n", taskStats.get("completed")));
|
||||
context.append(String.format("- Ausstehend: %d\n", taskStats.get("pending")));
|
||||
|
||||
// Top customers
|
||||
var topCustomers = statisticsService.getTopCustomersByRevenue(5);
|
||||
if (!topCustomers.isEmpty()) {
|
||||
context.append("\n**Top 5 Kunden nach Umsatz:**\n");
|
||||
for (var entry : topCustomers) {
|
||||
context.append(String.format("- %s: %.2f EUR\n",
|
||||
entry.getKey() != null ? entry.getKey() : "Unbekannt",
|
||||
entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return context.toString();
|
||||
}
|
||||
|
||||
private String buildListContext(String customerFilter, JobStatus statusFilter) {
|
||||
StringBuilder context = new StringBuilder();
|
||||
|
||||
// Job counts by status
|
||||
var statusCounts = statisticsService.getJobCountsByStatus();
|
||||
context.append("**Aktuelle Auftragsstatistiken:**\n");
|
||||
statusCounts.forEach((status, count) ->
|
||||
context.append(String.format("- %s: %d Aufträge\n", status.getDisplayName(), count)));
|
||||
List<Job> jobs;
|
||||
if (customerFilter != null && statusFilter != null) {
|
||||
jobs = statisticsService.getJobsByCustomerAndStatus(customerFilter, statusFilter);
|
||||
context.append(String.format("**Jobs für %s mit Status %s:**\n\n",
|
||||
customerFilter, statusFilter.getDisplayName()));
|
||||
} else if (customerFilter != null) {
|
||||
jobs = statisticsService.getJobsByCustomer(customerFilter);
|
||||
context.append(String.format("**Jobs für %s:**\n\n", customerFilter));
|
||||
} else if (statusFilter != null) {
|
||||
jobs = statisticsService.getJobsByStatus(statusFilter);
|
||||
context.append(String.format("**Jobs mit Status %s:**\n\n", statusFilter.getDisplayName()));
|
||||
} else {
|
||||
jobs = statisticsService.getLatestJobs(20);
|
||||
context.append("**Aktuelle Jobs:**\n\n");
|
||||
}
|
||||
|
||||
// Totals
|
||||
context.append(String.format("\n**Gesamtübersicht:**\n"));
|
||||
context.append(String.format("- Gesamtanzahl Aufträge: %d\n", statisticsService.getTotalJobCount()));
|
||||
context.append(String.format("- Abschlussrate: %.1f%%\n", statisticsService.getCompletionRate()));
|
||||
context.append(String.format("- Gesamtumsatz: %.2f EUR\n", statisticsService.getTotalRevenue()));
|
||||
context.append(String.format("Gefunden: %d Jobs\n\n", jobs.size()));
|
||||
|
||||
// Task statistics
|
||||
var taskStats = statisticsService.getTaskCompletionStats();
|
||||
context.append(String.format("\n**Aufgaben:**\n"));
|
||||
context.append(String.format("- Gesamt: %d\n", taskStats.get("total")));
|
||||
context.append(String.format("- Erledigt: %d\n", taskStats.get("completed")));
|
||||
context.append(String.format("- Ausstehend: %d\n", taskStats.get("pending")));
|
||||
|
||||
// Top customers
|
||||
var topCustomers = statisticsService.getTopCustomersByRevenue(5);
|
||||
if (!topCustomers.isEmpty()) {
|
||||
context.append("\n**Top 5 Kunden nach Umsatz:**\n");
|
||||
for (var entry : topCustomers) {
|
||||
context.append(String.format("- %s: %.2f EUR\n",
|
||||
entry.getKey() != null ? entry.getKey() : "Unbekannt",
|
||||
entry.getValue()));
|
||||
// Show job summaries (max 10)
|
||||
int shown = 0;
|
||||
for (Job job : jobs) {
|
||||
if (shown >= 10) {
|
||||
context.append(String.format("... und %d weitere Jobs\n", jobs.size() - 10));
|
||||
break;
|
||||
}
|
||||
context.append(String.format("- %s: %s (%s)\n",
|
||||
job.getJobNumber() != null ? job.getJobNumber() : "Ohne Nr.",
|
||||
job.getCustomerSelection() != null ? job.getCustomerSelection() : "Unbekannt",
|
||||
job.getStatus().getDisplayName()));
|
||||
shown++;
|
||||
}
|
||||
|
||||
return context.toString();
|
||||
@@ -319,37 +648,79 @@ public class AiStatisticsService {
|
||||
}
|
||||
|
||||
private String buildFallbackResponse(QueryAnalysis analysis) {
|
||||
String customer = analysis.customerFilter;
|
||||
JobStatus statusFilter = analysis.statusFilter;
|
||||
|
||||
return switch (analysis.queryType) {
|
||||
case "list" -> {
|
||||
List<Job> jobs;
|
||||
if (customer != null && statusFilter != null) {
|
||||
jobs = statisticsService.getJobsByCustomerAndStatus(customer, statusFilter);
|
||||
yield String.format("Es wurden %d Jobs für %s mit Status \"%s\" gefunden.",
|
||||
jobs.size(), customer, statusFilter.getDisplayName());
|
||||
} else if (customer != null) {
|
||||
jobs = statisticsService.getJobsByCustomer(customer);
|
||||
yield String.format("Es wurden %d Jobs für %s gefunden.", jobs.size(), customer);
|
||||
} else if (statusFilter != null) {
|
||||
jobs = statisticsService.getJobsByStatus(statusFilter);
|
||||
yield String.format("Es wurden %d Jobs mit Status \"%s\" gefunden.",
|
||||
jobs.size(), statusFilter.getDisplayName());
|
||||
} else {
|
||||
yield "Hier sind die aktuellen Jobs.";
|
||||
}
|
||||
}
|
||||
case "status" -> {
|
||||
var counts = statisticsService.getJobCountsByStatus();
|
||||
StringBuilder sb = new StringBuilder("**Auftragsübersicht nach Status:**\n\n");
|
||||
var counts = customer != null
|
||||
? statisticsService.getJobCountsByStatusForCustomer(customer)
|
||||
: statisticsService.getJobCountsByStatus();
|
||||
String title = customer != null
|
||||
? String.format("**Auftragsübersicht für %s:**\n\n", customer)
|
||||
: "**Auftragsübersicht nach Status:**\n\n";
|
||||
StringBuilder sb = new StringBuilder(title);
|
||||
counts.forEach((status, count) -> {
|
||||
if (count > 0) {
|
||||
sb.append(String.format("- **%s:** %d Aufträge\n", status.getDisplayName(), count));
|
||||
}
|
||||
});
|
||||
sb.append(String.format("\n**Gesamt:** %d Aufträge", statisticsService.getTotalJobCount()));
|
||||
long total = customer != null
|
||||
? statisticsService.getTotalJobCountForCustomer(customer)
|
||||
: statisticsService.getTotalJobCount();
|
||||
sb.append(String.format("\n**Gesamt:** %d Aufträge", total));
|
||||
yield sb.toString();
|
||||
}
|
||||
case "revenue" -> {
|
||||
var topCustomers = statisticsService.getTopCustomersByRevenue(5);
|
||||
StringBuilder sb = new StringBuilder("**Top Kunden nach Umsatz:**\n\n");
|
||||
int rank = 1;
|
||||
for (var entry : topCustomers) {
|
||||
sb.append(String.format("%d. **%s:** %.2f EUR\n",
|
||||
rank++,
|
||||
entry.getKey() != null ? entry.getKey() : "Unbekannt",
|
||||
entry.getValue()));
|
||||
if (customer != null) {
|
||||
var revenue = statisticsService.getTotalRevenueForCustomer(customer);
|
||||
var jobCount = statisticsService.getTotalJobCountForCustomer(customer);
|
||||
yield String.format("**Umsatz für %s:**\n\n" +
|
||||
"- **Gesamtumsatz:** %.2f EUR\n" +
|
||||
"- **Aufträge:** %d",
|
||||
customer, revenue, jobCount);
|
||||
} else {
|
||||
var topCustomers = statisticsService.getTopCustomersByRevenue(5);
|
||||
StringBuilder sb = new StringBuilder("**Top Kunden nach Umsatz:**\n\n");
|
||||
int rank = 1;
|
||||
for (var entry : topCustomers) {
|
||||
sb.append(String.format("%d. **%s:** %.2f EUR\n",
|
||||
rank++,
|
||||
entry.getKey() != null ? entry.getKey() : "Unbekannt",
|
||||
entry.getValue()));
|
||||
}
|
||||
sb.append(String.format("\n**Gesamtumsatz:** %.2f EUR", statisticsService.getTotalRevenue()));
|
||||
yield sb.toString();
|
||||
}
|
||||
sb.append(String.format("\n**Gesamtumsatz:** %.2f EUR", statisticsService.getTotalRevenue()));
|
||||
yield sb.toString();
|
||||
}
|
||||
case "trend" -> {
|
||||
int year = Year.now().getValue();
|
||||
var monthly = statisticsService.getMonthlyJobCounts(year);
|
||||
var monthly = customer != null
|
||||
? statisticsService.getMonthlyJobCountsForCustomer(year, customer)
|
||||
: statisticsService.getMonthlyJobCounts(year);
|
||||
long total = monthly.values().stream().mapToLong(Long::longValue).sum();
|
||||
yield String.format("**Monatstrend %d:**\n\nInsgesamt wurden %d Aufträge erstellt. " +
|
||||
"Die Verteilung ist im Diagramm ersichtlich.", year, total);
|
||||
String title = customer != null
|
||||
? String.format("**Monatstrend %d für %s:**", year, customer)
|
||||
: String.format("**Monatstrend %d:**", year);
|
||||
yield String.format("%s\n\nInsgesamt wurden %d Aufträge erstellt. " +
|
||||
"Die Verteilung ist im Diagramm ersichtlich.", title, total);
|
||||
}
|
||||
case "tasks" -> {
|
||||
var taskStats = statisticsService.getTaskCompletionStats();
|
||||
@@ -363,13 +734,24 @@ public class AiStatisticsService {
|
||||
total, completed, rate, taskStats.getOrDefault("pending", 0L));
|
||||
}
|
||||
default -> {
|
||||
yield String.format("**Übersicht:**\n\n" +
|
||||
"- **Aufträge gesamt:** %d\n" +
|
||||
"- **Abschlussrate:** %.1f%%\n" +
|
||||
"- **Gesamtumsatz:** %.2f EUR",
|
||||
statisticsService.getTotalJobCount(),
|
||||
statisticsService.getCompletionRate(),
|
||||
statisticsService.getTotalRevenue());
|
||||
if (customer != null) {
|
||||
yield String.format("**Übersicht für %s:**\n\n" +
|
||||
"- **Aufträge gesamt:** %d\n" +
|
||||
"- **Abschlussrate:** %.1f%%\n" +
|
||||
"- **Umsatz:** %.2f EUR",
|
||||
customer,
|
||||
statisticsService.getTotalJobCountForCustomer(customer),
|
||||
statisticsService.getCompletionRateForCustomer(customer),
|
||||
statisticsService.getTotalRevenueForCustomer(customer));
|
||||
} else {
|
||||
yield String.format("**Übersicht:**\n\n" +
|
||||
"- **Aufträge gesamt:** %d\n" +
|
||||
"- **Abschlussrate:** %.1f%%\n" +
|
||||
"- **Gesamtumsatz:** %.2f EUR",
|
||||
statisticsService.getTotalJobCount(),
|
||||
statisticsService.getCompletionRate(),
|
||||
statisticsService.getTotalRevenue());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,16 +106,23 @@ public class LlmRestClient {
|
||||
}
|
||||
|
||||
private String extractContent(String response) {
|
||||
if (response == null) {
|
||||
if (response == null || response.isBlank()) {
|
||||
log.warn("LLM returned null or blank response");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(response);
|
||||
JsonNode choices = root.path("choices");
|
||||
if (choices.isArray() && !choices.isEmpty()) {
|
||||
return choices.get(0).path("message").path("content").asText();
|
||||
String content = choices.get(0).path("message").path("content").asText();
|
||||
// asText() returns empty string for null/missing nodes - treat as null
|
||||
if (content == null || content.isBlank()) {
|
||||
log.warn("LLM response content is empty");
|
||||
return null;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
log.warn("Unexpected response structure: {}", response);
|
||||
log.warn("Unexpected response structure (no choices): {}", response);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.error("Error parsing LLM response: {}", e.getMessage());
|
||||
|
||||
@@ -262,7 +262,11 @@ public class StatisticsView extends VerticalLayout {
|
||||
// Text Response
|
||||
Div textDiv = new Div();
|
||||
textDiv.getStyle().set("margin-top", "var(--lumo-space-s)");
|
||||
textDiv.getElement().setProperty("innerHTML", formatMarkdown(response.textResponse()));
|
||||
String responseText = response.textResponse();
|
||||
if (responseText == null || responseText.isBlank()) {
|
||||
responseText = "*Die Statistikdaten wurden erfolgreich abgerufen und werden im Diagramm angezeigt.*";
|
||||
}
|
||||
textDiv.getElement().setProperty("innerHTML", formatMarkdown(responseText));
|
||||
bubble.add(textDiv);
|
||||
|
||||
// Chart wenn vorhanden
|
||||
@@ -505,7 +509,7 @@ public class StatisticsView extends VerticalLayout {
|
||||
}
|
||||
}
|
||||
""";
|
||||
case "doughnut", "pie" -> """
|
||||
case "doughnut" -> """
|
||||
{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
@@ -535,6 +539,118 @@ public class StatisticsView extends VerticalLayout {
|
||||
}
|
||||
}
|
||||
""";
|
||||
case "pie" -> """
|
||||
{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 15,
|
||||
font: { size: 12 }
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||
titleFont: { size: 14, weight: 'bold' },
|
||||
bodyFont: { size: 13 },
|
||||
padding: 12,
|
||||
cornerRadius: 8
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
animateRotate: true,
|
||||
animateScale: true,
|
||||
duration: 1000,
|
||||
easing: 'easeOutQuart'
|
||||
}
|
||||
}
|
||||
""";
|
||||
case "radar" -> """
|
||||
{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 15
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||
titleFont: { size: 14, weight: 'bold' },
|
||||
bodyFont: { size: 13 },
|
||||
padding: 12,
|
||||
cornerRadius: 8
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
r: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0,0,0,0.1)'
|
||||
},
|
||||
pointLabels: {
|
||||
font: { size: 12 }
|
||||
}
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
borderWidth: 2
|
||||
},
|
||||
point: {
|
||||
radius: 4,
|
||||
hoverRadius: 6
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
duration: 1000,
|
||||
easing: 'easeOutQuart'
|
||||
}
|
||||
}
|
||||
""";
|
||||
case "polarArea" -> """
|
||||
{
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
padding: 15,
|
||||
font: { size: 12 }
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0,0,0,0.8)',
|
||||
titleFont: { size: 14, weight: 'bold' },
|
||||
bodyFont: { size: 13 },
|
||||
padding: 12,
|
||||
cornerRadius: 8
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
r: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0,0,0,0.1)'
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
animateRotate: true,
|
||||
animateScale: true,
|
||||
duration: 1000,
|
||||
easing: 'easeOutQuart'
|
||||
}
|
||||
}
|
||||
""";
|
||||
default -> """
|
||||
{
|
||||
responsive: true,
|
||||
|
||||
@@ -65,6 +65,12 @@ public interface JobRepository extends MongoRepository<Job, ObjectId> {
|
||||
*/
|
||||
List<Job> findByCustomerSelection(String customerSelection);
|
||||
|
||||
/**
|
||||
* Findet Aufträge anhand einer Kundenauswahl (case-insensitive)
|
||||
*/
|
||||
@Query("{ 'customerSelection': { $regex: ?0, $options: 'i' } }")
|
||||
List<Job> findByCustomerSelectionIgnoreCase(String customerSelection);
|
||||
|
||||
/**
|
||||
* Prüft, ob eine Auftragsnummer bereits existiert
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Service for job statistics and aggregations.
|
||||
@@ -142,10 +143,29 @@ public class JobStatisticsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get jobs by customer selection.
|
||||
* Get jobs by customer selection (case-insensitive, flexible matching).
|
||||
*/
|
||||
public List<Job> getJobsByCustomer(String customer) {
|
||||
return jobRepository.findByCustomerSelection(customer);
|
||||
if (customer == null || customer.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
// Trim and escape regex special characters for MongoDB
|
||||
String trimmedCustomer = customer.trim();
|
||||
String escapedCustomer = trimmedCustomer.replaceAll("([.^$*+?()\\[\\]{}|\\\\])", "\\\\$1");
|
||||
|
||||
// First try exact match (with optional whitespace)
|
||||
String exactRegex = "^\\s*" + escapedCustomer + "\\s*$";
|
||||
List<Job> jobs = jobRepository.findByCustomerSelectionIgnoreCase(exactRegex);
|
||||
log.debug("getJobsByCustomer('{}') - exact regex: '{}' - found {} jobs", customer, exactRegex, jobs.size());
|
||||
|
||||
// If no exact match, try partial match (customer name contains the search term)
|
||||
if (jobs.isEmpty()) {
|
||||
String containsRegex = ".*" + escapedCustomer + ".*";
|
||||
jobs = jobRepository.findByCustomerSelectionIgnoreCase(containsRegex);
|
||||
log.debug("getJobsByCustomer('{}') - contains regex: '{}' - found {} jobs", customer, containsRegex, jobs.size());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,4 +220,227 @@ public class JobStatisticsService {
|
||||
public List<Job> getLatestJobs(int limit) {
|
||||
return jobRepository.findLatestJobs().stream().limit(limit).toList();
|
||||
}
|
||||
|
||||
// ==================== Filtered Statistics Methods ====================
|
||||
|
||||
/**
|
||||
* Get all available customer names for autocomplete/filtering.
|
||||
*/
|
||||
public List<String> getAllCustomerNames() {
|
||||
return jobRepository.findAll().stream()
|
||||
.map(Job::getCustomerSelection)
|
||||
.filter(c -> c != null && !c.isBlank())
|
||||
.distinct()
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get job counts by status filtered by customer.
|
||||
*/
|
||||
public Map<JobStatus, Long> getJobCountsByStatusForCustomer(String customer) {
|
||||
List<Job> customerJobs = getJobsByCustomer(customer);
|
||||
Map<JobStatus, Long> counts = new EnumMap<>(JobStatus.class);
|
||||
for (JobStatus status : JobStatus.values()) {
|
||||
counts.put(status, 0L);
|
||||
}
|
||||
for (Job job : customerJobs) {
|
||||
counts.computeIfPresent(job.getStatus(), (k, v) -> v + 1L);
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total job count for a customer.
|
||||
*/
|
||||
public long getTotalJobCountForCustomer(String customer) {
|
||||
return getJobsByCustomer(customer).size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total revenue for a customer.
|
||||
*/
|
||||
public BigDecimal getTotalRevenueForCustomer(String customer) {
|
||||
return getJobsByCustomer(customer).stream()
|
||||
.map(Job::getPrice)
|
||||
.filter(price -> price != null)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get completion rate for a customer.
|
||||
*/
|
||||
public double getCompletionRateForCustomer(String customer) {
|
||||
List<Job> customerJobs = getJobsByCustomer(customer);
|
||||
if (customerJobs.isEmpty()) {
|
||||
return 0.0;
|
||||
}
|
||||
long completed = customerJobs.stream()
|
||||
.filter(j -> j.getStatus() == JobStatus.COMPLETED)
|
||||
.count();
|
||||
return (double) completed / customerJobs.size() * 100.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get monthly job counts for a customer in a specific year.
|
||||
*/
|
||||
public Map<Month, Long> getMonthlyJobCountsForCustomer(int year, String customer) {
|
||||
Map<Month, Long> monthlyCounts = new LinkedHashMap<>();
|
||||
LocalDateTime yearStart = LocalDateTime.of(year, 1, 1, 0, 0);
|
||||
LocalDateTime yearEnd = LocalDateTime.of(year, 12, 31, 23, 59, 59);
|
||||
|
||||
List<Job> customerJobs = getJobsByCustomer(customer).stream()
|
||||
.filter(j -> j.getCreatedAt() != null
|
||||
&& !j.getCreatedAt().isBefore(yearStart)
|
||||
&& !j.getCreatedAt().isAfter(yearEnd))
|
||||
.toList();
|
||||
|
||||
// Initialize all months with 0
|
||||
for (Month month : Month.values()) {
|
||||
monthlyCounts.put(month, 0L);
|
||||
}
|
||||
|
||||
// Count jobs per month
|
||||
for (Job job : customerJobs) {
|
||||
Month month = job.getCreatedAt().getMonth();
|
||||
monthlyCounts.computeIfPresent(month, (k, v) -> v + 1L);
|
||||
}
|
||||
|
||||
return monthlyCounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get jobs filtered by customer and optionally by status.
|
||||
*/
|
||||
public List<Job> getJobsByCustomerAndStatus(String customer, JobStatus status) {
|
||||
List<Job> customerJobs = getJobsByCustomer(customer);
|
||||
if (status == null) {
|
||||
return customerJobs;
|
||||
}
|
||||
return customerJobs.stream()
|
||||
.filter(j -> j.getStatus() == status)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find best matching customer name from query (fuzzy matching).
|
||||
*/
|
||||
public String findMatchingCustomer(String query) {
|
||||
if (query == null || query.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String lowerQuery = query.toLowerCase();
|
||||
List<String> allCustomers = getAllCustomerNames();
|
||||
log.debug("findMatchingCustomer - Query: '{}', Available customers: {}", query, allCustomers);
|
||||
|
||||
// First: exact match (case insensitive)
|
||||
for (String customer : allCustomers) {
|
||||
if (customer.equalsIgnoreCase(query)) {
|
||||
log.debug("findMatchingCustomer - Exact match found: '{}'", customer);
|
||||
return customer;
|
||||
}
|
||||
}
|
||||
|
||||
// Second: check if query contains a customer name
|
||||
for (String customer : allCustomers) {
|
||||
String lowerCustomer = customer.toLowerCase();
|
||||
if (lowerQuery.contains(lowerCustomer)) {
|
||||
log.debug("findMatchingCustomer - Query contains customer: '{}'", customer);
|
||||
return customer;
|
||||
}
|
||||
}
|
||||
|
||||
// Third: extract potential customer name from query and check if it matches a customer
|
||||
// This handles cases like "cAPPacity GmbH" matching "cAPPacity GmbH & Co. KG"
|
||||
String extractedName = extractCustomerNameFromQuery(query);
|
||||
if (extractedName != null) {
|
||||
String lowerExtracted = extractedName.toLowerCase();
|
||||
for (String customer : allCustomers) {
|
||||
String lowerCustomer = customer.toLowerCase();
|
||||
// Check if customer name starts with the extracted name, or contains it
|
||||
if (lowerCustomer.startsWith(lowerExtracted) || lowerCustomer.contains(lowerExtracted)) {
|
||||
log.debug("findMatchingCustomer - Extracted name '{}' matches customer: '{}'", extractedName, customer);
|
||||
return customer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fourth: word match (any significant word from the query matches customer name)
|
||||
String[] queryWords = lowerQuery.split("\\s+");
|
||||
for (String customer : allCustomers) {
|
||||
String lowerCustomer = customer.toLowerCase();
|
||||
for (String word : queryWords) {
|
||||
// Skip common words and short words
|
||||
if (word.length() >= 4 && !isCommonWord(word) && lowerCustomer.contains(word)) {
|
||||
log.debug("findMatchingCustomer - Word match found: '{}' in '{}'", word, customer);
|
||||
return customer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("findMatchingCustomer - No match found for query: '{}'", query);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract potential customer name from a query string.
|
||||
* Looks for patterns like "firma X", "kunde X", "für X", etc.
|
||||
*/
|
||||
private String extractCustomerNameFromQuery(String query) {
|
||||
String lowerQuery = query.toLowerCase();
|
||||
|
||||
// Patterns that typically precede a customer name
|
||||
String[] patterns = {
|
||||
"firma ", "kunde ", "kunden ", "unternehmen ",
|
||||
"für die firma ", "für den kunden ", "von der firma ", "vom kunden ",
|
||||
"der firma ", "des kunden ", "bei "
|
||||
};
|
||||
|
||||
for (String pattern : patterns) {
|
||||
int idx = lowerQuery.indexOf(pattern);
|
||||
if (idx >= 0) {
|
||||
// Extract everything after the pattern
|
||||
int startIdx = idx + pattern.length();
|
||||
String afterPattern = query.substring(startIdx).trim();
|
||||
|
||||
// Remove common trailing words
|
||||
afterPattern = removeTrailingCommonWords(afterPattern);
|
||||
|
||||
if (!afterPattern.isBlank()) {
|
||||
log.debug("extractCustomerNameFromQuery - Found potential name: '{}'", afterPattern);
|
||||
return afterPattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove common trailing words from extracted customer name.
|
||||
*/
|
||||
private String removeTrailingCommonWords(String text) {
|
||||
String[] trailingPatterns = {
|
||||
" an$", " anzeigen$", " zeigen$", " auflisten$", " liste$",
|
||||
" status$", " mit status$", " die$", " der$", " das$"
|
||||
};
|
||||
|
||||
String result = text;
|
||||
for (String pattern : trailingPatterns) {
|
||||
result = result.replaceAll("(?i)" + pattern, "").trim();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a word is a common German/English word that should be ignored in matching.
|
||||
*/
|
||||
private boolean isCommonWord(String word) {
|
||||
return Set.of(
|
||||
"zeige", "alle", "jobs", "der", "die", "das", "für", "von", "mit", "und",
|
||||
"firma", "kunde", "status", "welche", "sind", "gmbh", "show", "all", "the"
|
||||
).contains(word.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user