diff --git a/Visualization.md b/Visualization.md index e7e5b2f..b5f3e11 100644 --- a/Visualization.md +++ b/Visualization.md @@ -245,8 +245,10 @@ externe Datenbankserver (nur PHP/Python-Standardbibliothek) auskommen, um die Klick-Modals. ✅ erledigt: Events/Systeme/KPIs kommen live von der API, ein neuer SSE-Endpoint (`GET /api/v1/stream`, F13) stoesst Frontend-Updates an (Polling-Fallback in `office-realtime.js`), KPI-Monitor zeichnet eine eigene SVG-Sparkline (F10, kein Chart-Lib) - sobald KPIs gemeldet wurden - vorher weiterhin der Platzhalter aus Phase 2 (F7 ist - inhaltlich weiterhin offen). + sobald KPIs gemeldet wurden. "Agenten" (registrierte Agenten) und "Online" (davon + praesent, isAgentPresent()) sind als erste zwei KPI-Kacheln immer sichtbar, direkt aus der + Agentenliste abgeleitet statt gemeldet (Issue #39) - darueber hinaus ist F7 inhaltlich + weiterhin offen, ohne gemeldete KPIs zeigt die Wand nur diese beiden Kacheln. 6. **Phase 5 – Agenten & Animation**: Zustandsmaschine, Laufwege, Tipp-/Lese-/Kaffee-Animation, Reaktion auf neue Ticket-/Event-/Systemstatus-Meldungen. ✅ erledigt: `resolveAgentVisualState` bildet die von der API gemeldeten Zustaende auf Ort/Pose ab, Agenten-Avatare sind eine diff --git a/html/js/office-render.js b/html/js/office-render.js index eb2578d..f408c9c 100644 --- a/html/js/office-render.js +++ b/html/js/office-render.js @@ -24,6 +24,7 @@ import { buildSystemModalContent, buildKpiTiles, buildKpiModalContent, + buildAgentCountKpis, mapAgentsToDesks, normalizeAgentForDisplay, resolveAgentVisualState, @@ -1401,6 +1402,11 @@ export function renderOfficeScene(container, data = {}) { // is_present-Feld wie registrierte Agenten (Issue #19). poIsLive = isLive; + // "Agenten"/"Online" (Issue #39) stehen vor den gemeldeten KPIs (F7), damit sie auf der + // Wandkachel (MAX_KPI_TILES) immer sichtbar sind, statt von gemeldeten Kennzahlen verdraengt + // zu werden. + const displayKpis = [...buildAgentCountKpis(agents), ...kpis]; + const groupedTickets = groupTicketsByColumn(tickets); const boardGroup = renderBoard(ticketCounts, groupedTickets, (columnKey) => { @@ -1417,10 +1423,10 @@ export function renderOfficeScene(container, data = {}) { svg.insertBefore(boardGroup, svg.querySelector(".agents-layer")); } - const wallGroup = renderMonitorWall(systems, events, kpis, { + const wallGroup = renderMonitorWall(systems, events, displayKpis, { onEventsClick: () => openModal(buildEventModalContent(events), renderEventModalBody), onSystemsClick: () => openModal(buildSystemModalContent(systems), renderSystemModalBody), - onKpiClick: () => openModal(buildKpiModalContent(kpis), renderKpiModalBody), + onKpiClick: () => openModal(buildKpiModalContent(displayKpis), renderKpiModalBody), }); const existingWall = svg.querySelector(".monitor-wall"); if (existingWall) { diff --git a/html/js/office-scene.js b/html/js/office-scene.js index 8c99b12..f769205 100644 --- a/html/js/office-scene.js +++ b/html/js/office-scene.js @@ -415,6 +415,27 @@ export function isAgentPresent(agent) { return Boolean(agent.is_present); } +/** + * Die beiden im KPI-Monitor erwarteten Kennzahlen "Agenten" (registrierte Agenten) und + * "Online" (davon innerhalb der letzten 10 Minuten aktiv, siehe isAgentPresent()) werden + * anders als die per POST /api/v1/kpis/{key} gemeldeten Kennzahlen (F7, weiterhin offen) + * direkt aus der ohnehin geladenen Agentenliste abgeleitet statt separat gemeldet. + * Gibt KPI-foermige Eintraege zurueck (gleiche Form wie GET /api/v1/kpis), damit sie sich + * mit den gemeldeten KPIs vor buildKpiTiles()/buildKpiModalContent() zusammenfuehren lassen. + * + * @param {Array} agents - z.B. leere Schreibtisch-Plaetze der Demo-Daten sind null. + * @returns {Array} + */ +export function buildAgentCountKpis(agents = []) { + const registered = agents.filter(Boolean); + const online = registered.filter(isAgentPresent).length; + + return [ + { key: "agents_total", label: "Agenten", unit: "", current_value: registered.length, history: [] }, + { key: "agents_online", label: "Online", unit: "", current_value: online, history: [] }, + ]; +} + /** * Ordnet eine rohe Agentenliste (GET /api/v1/agents) den Schreibtischen zu - anders als * buildDeskLayout()/Demo-Daten steht der Sitzplatz hier nicht in der Array-Reihenfolge, diff --git a/tests/frontend/office-scene.test.js b/tests/frontend/office-scene.test.js index 44afce0..60014fd 100644 --- a/tests/frontend/office-scene.test.js +++ b/tests/frontend/office-scene.test.js @@ -20,6 +20,7 @@ import { buildKpiSparklinePoints, buildKpiTiles, buildKpiModalContent, + buildAgentCountKpis, MAX_KPI_TILES, AGENT_LOCATIONS, resolveAgentVisualState, @@ -331,6 +332,29 @@ test("buildKpiModalContent zeigt alle KPIs (kein Limit) inkl. Historie", () => { assert.equal(content.items[0].label, "Offene Tickets"); }); +test("buildAgentCountKpis liefert Anzahl registrierter und online-Agenten (Issue #39)", () => { + const kpis = buildAgentCountKpis([ + { id: 1, is_present: true }, + { id: 2, is_present: false }, + { id: 3, is_present: true }, + ]); + assert.equal(kpis.length, 2); + assert.deepEqual(kpis[0], { key: "agents_total", label: "Agenten", unit: "", current_value: 3, history: [] }); + assert.deepEqual(kpis[1], { key: "agents_online", label: "Online", unit: "", current_value: 2, history: [] }); +}); + +test("buildAgentCountKpis ignoriert leere Schreibtisch-Plaetze (null) und faellt ohne is_present auf anwesend zurueck", () => { + const kpis = buildAgentCountKpis([{ id: 1 }, null, { id: 2 }, null]); + assert.equal(kpis[0].current_value, 2); + assert.equal(kpis[1].current_value, 2); +}); + +test("buildAgentCountKpis liefert 0/0 ohne Agenten", () => { + const kpis = buildAgentCountKpis([]); + assert.equal(kpis[0].current_value, 0); + assert.equal(kpis[1].current_value, 0); +}); + // --------------------------------------------------------------------------------------- // Phase 5 - Agenten-Zustandsmaschine & Animation // ---------------------------------------------------------------------------------------