// Zeichnet die statische Buero-Szene (Phase 2, siehe Visualization.md Abschnitt 9) als // Vanilla-SVG in den Container #office-scene. Keine Animation, keine Klick-Interaktion // (Board-/Monitor-Modals kommen erst mit den Live-Daten in Phase 3/4). Reine DOM-Glue-Logik - // die eigentliche Layoutberechnung steckt testbar in office-scene.js. import { ROLES, buildBoardColumnLayout, buildDeskLayout, buildSystemTiles, buildEventLines, } from "./office-scene.js"; import { DEMO_TICKET_COUNTS, DEMO_AGENTS, DEMO_SYSTEMS, DEMO_EVENTS, DEMO_KPI_PLACEHOLDER, } from "./demo-data.js"; const SVG_NS = "http://www.w3.org/2000/svg"; const VIEW_W = 1920; const VIEW_H = 1080; const WALL_BAND_H = 420; const BOARD_FRAME = { x: 60, y: 40, w: 1120, h: 340 }; const MONITOR_WALL = { x: 1240, y: 40, w: 620, h: 340 }; const DOOR = { w: 40, h: 220, y: 700 }; const DESK = { w: 220, h: 110 }; const DESK_ROW_Y = [540, 840]; const DESK_COL_X = [232, 644, 1056, 1468]; const NOTE_W = 96; const NOTE_H = 50; const NOTE_STEP_Y = 56; function el(tag, attrs = {}, children = []) { const node = document.createElementNS(SVG_NS, tag); for (const [key, value] of Object.entries(attrs)) { node.setAttribute(key, value); } for (const child of children) { node.appendChild(child); } return node; } function text(content, attrs = {}) { const node = el("text", attrs); node.textContent = content; return node; } function renderRoom() { const g = el("g", { class: "office-room" }); g.appendChild(el("rect", { x: 0, y: 0, width: VIEW_W, height: VIEW_H, class: "floor" })); g.appendChild(el("rect", { x: 0, y: 0, width: VIEW_W, height: WALL_BAND_H, class: "wall" })); g.appendChild(el("rect", { x: 0, y: 0, width: 40, height: VIEW_H, class: "wall wall-side" })); g.appendChild(el("rect", { x: VIEW_W - 40, y: 0, width: 40, height: VIEW_H, class: "wall wall-side" })); g.appendChild(el("rect", { x: 0, y: DOOR.y, width: DOOR.w, height: DOOR.h, class: "door" })); g.appendChild( el("rect", { x: VIEW_W - DOOR.w, y: DOOR.y, width: DOOR.w, height: DOOR.h, class: "door" }) ); return g; } function renderStackBadge(x, y, count) { const g = el("g", { class: "note-stack" }); // Mehrere leicht versetzte Zettel als "Stapel"-Andeutung, darueber die Anzahl (F3). [6, 3, 0].forEach((offset, i) => { g.appendChild( el("rect", { x: x - offset, y: y - offset, width: NOTE_W, height: NOTE_H, rx: 6, class: `note note-stack-layer-${i}`, }) ); }); const cx = x + NOTE_W / 2; const cy = y + NOTE_H / 2; g.appendChild(el("circle", { cx, cy, r: 20, class: "note-stack-badge" })); g.appendChild( text(String(count), { x: cx, y: cy + 7, class: "note-stack-count", "text-anchor": "middle" }) ); return g; } function renderBoard(ticketCounts) { const columns = buildBoardColumnLayout(ticketCounts); const g = el("g", { class: "board" }); g.appendChild( el("rect", { ...BOARD_FRAME, rx: 10, class: "board-frame" }) ); g.appendChild( text("Kanban-Board", { x: BOARD_FRAME.x + 16, y: BOARD_FRAME.y - 14, class: "wall-label" }) ); const columnWidth = BOARD_FRAME.w / columns.length; columns.forEach((column, index) => { const columnX = BOARD_FRAME.x + index * columnWidth; const colGroup = el("g", { class: `board-column board-column-${column.key}` }); if (index > 0) { colGroup.appendChild( el("line", { x1: columnX, y1: BOARD_FRAME.y, x2: columnX, y2: BOARD_FRAME.y + BOARD_FRAME.h, class: "board-column-divider", }) ); } colGroup.appendChild( text(column.label, { x: columnX + columnWidth / 2, y: BOARD_FRAME.y + 26, class: "board-column-label", "text-anchor": "middle", }) ); const noteX = columnX + (columnWidth - NOTE_W) / 2; let noteY = BOARD_FRAME.y + 44; for (let i = 0; i < column.visibleNotes; i++) { colGroup.appendChild( el("rect", { x: noteX, y: noteY, width: NOTE_W, height: NOTE_H, rx: 6, class: "note" }) ); noteY += NOTE_STEP_Y; } if (column.stackCount > 0) { colGroup.appendChild(renderStackBadge(noteX, noteY, column.stackCount)); } g.appendChild(colGroup); }); return g; } function renderEventMonitor(frame, events) { const lines = buildEventLines(events); const g = el("g", { class: "monitor monitor-events" }); g.appendChild(el("rect", { ...frame, rx: 8, class: "monitor-frame" })); g.appendChild(text("Events", { x: frame.x + 12, y: frame.y - 10, class: "wall-label" })); g.appendChild( el("rect", { x: frame.x + 10, y: frame.y + 10, width: frame.w - 20, height: frame.h - 20, class: "monitor-screen", }) ); lines.forEach((eventLine, index) => { g.appendChild( text(`${eventLine.time} ${eventLine.system}: ${eventLine.message}`, { x: frame.x + 18, y: frame.y + 38 + index * 26, class: `monitor-event-line severity-${eventLine.severity}`, }) ); }); return g; } function renderSystemMonitor(frame, systems) { const tiles = buildSystemTiles(systems); const g = el("g", { class: "monitor monitor-systems" }); g.appendChild(el("rect", { ...frame, rx: 8, class: "monitor-frame" })); g.appendChild(text("Systeme", { x: frame.x + 12, y: frame.y - 10, class: "wall-label" })); g.appendChild( el("rect", { x: frame.x + 10, y: frame.y + 10, width: frame.w - 20, height: frame.h - 20, class: "monitor-screen", }) ); const cols = 3; const tileMargin = 8; const gridX = frame.x + 18; const gridY = frame.y + 18; const tileSize = (frame.w - 36 - tileMargin * (cols - 1)) / cols; tiles.forEach((system, index) => { const col = index % cols; const row = Math.floor(index / cols); const tileX = gridX + col * (tileSize + tileMargin); const tileY = gridY + row * (tileSize + tileMargin); const tileGroup = el("g", { class: `system-tile status-${system.status}` }); tileGroup.appendChild( el("rect", { x: tileX, y: tileY, width: tileSize, height: tileSize, rx: 6, class: "system-tile-bg" }) ); // Einfaches "Server"-Icon: zwei Balken mit je einer Status-Leuchte (F6: ein Icon-Typ // fuer alle Systeme, unabhaengig von der Art). tileGroup.appendChild( el("rect", { x: tileX + 10, y: tileY + 10, width: tileSize - 20, height: 14, rx: 2, class: "system-tile-bar" }) ); tileGroup.appendChild( el("rect", { x: tileX + 10, y: tileY + 30, width: tileSize - 20, height: 14, rx: 2, class: "system-tile-bar" }) ); tileGroup.appendChild(el("circle", { cx: tileX + 18, cy: tileY + 17, r: 3, class: "system-tile-led" })); tileGroup.appendChild(el("circle", { cx: tileX + 18, cy: tileY + 37, r: 3, class: "system-tile-led" })); tileGroup.appendChild( text(system.name, { x: tileX + tileSize / 2, y: tileY + tileSize - 8, class: "system-tile-label", "text-anchor": "middle", }) ); g.appendChild(tileGroup); }); return g; } function renderKpiMonitor(frame, placeholder) { const g = el("g", { class: "monitor monitor-kpi" }); g.appendChild(el("rect", { ...frame, rx: 8, class: "monitor-frame" })); g.appendChild(text("KPIs", { x: frame.x + 12, y: frame.y - 10, class: "wall-label" })); g.appendChild( el("rect", { x: frame.x + 10, y: frame.y + 10, width: frame.w - 20, height: frame.h - 20, class: "monitor-screen monitor-screen-placeholder", }) ); const words = placeholder.split(" "); const lineHeight = 22; const maxCharsPerLine = 16; const lines = []; let current = ""; for (const word of words) { const candidate = current ? `${current} ${word}` : word; if (candidate.length > maxCharsPerLine && current) { lines.push(current); current = word; } else { current = candidate; } } if (current) lines.push(current); const startY = frame.y + frame.h / 2 - ((lines.length - 1) * lineHeight) / 2; lines.forEach((line, index) => { g.appendChild( text(line, { x: frame.x + frame.w / 2, y: startY + index * lineHeight, class: "monitor-placeholder-text", "text-anchor": "middle", }) ); }); return g; } function renderMonitorWall(systems, events) { const g = el("g", { class: "monitor-wall" }); const gap = 20; const monitorWidth = (MONITOR_WALL.w - gap * 2) / 3; const frames = [0, 1, 2].map((i) => ({ x: MONITOR_WALL.x + i * (monitorWidth + gap), y: MONITOR_WALL.y, w: monitorWidth, h: MONITOR_WALL.h, })); g.appendChild(renderEventMonitor(frames[0], events)); g.appendChild(renderSystemMonitor(frames[1], systems)); g.appendChild(renderKpiMonitor(frames[2], DEMO_KPI_PLACEHOLDER)); return g; } function personPose(pose) { // Rein statische Pose-Auswahl (kein Animationszustand) - Zustandsmaschine/Animation // folgt erst in Phase 5. switch (pose) { case "reading_newspaper": return "pose-newspaper"; case "coffee": return "pose-coffee"; case "working": return "pose-working"; default: return "pose-idle"; } } function renderDesk({ deskIndex, agent }) { const col = deskIndex % DESK_COL_X.length; const row = Math.floor(deskIndex / DESK_COL_X.length); const x = DESK_COL_X[col]; const y = DESK_ROW_Y[row]; const role = agent ? ROLES[agent.role] : null; const g = el("g", { class: `desk ${agent ? "desk-occupied" : "desk-empty"}`, transform: `translate(${x}, ${y})`, }); g.appendChild(el("rect", { x: 0, y: 40, width: DESK.w, height: DESK.h, rx: 6, class: "desk-table" })); g.appendChild( el("rect", { x: 20, y: 10, width: 60, height: 40, rx: 4, class: `desk-screen ${agent ? personPose(agent.pose) : ""}` }) ); g.appendChild(el("circle", { cx: DESK.w / 2, cy: 40 + DESK.h + 30, r: 22, class: "desk-chair" })); if (role) { // Deko-Icon je Rolle (F1): Stern (Entwickler:in), Bluete (Product Owner:in), Haken (QA). if (role.deco === "star") { g.appendChild( el("path", { d: "M170,20 l4,10 11,0 -9,7 3,11 -9,-7 -9,7 3,-11 -9,-7 11,0 z", class: "desk-deco desk-deco-star", }) ); } else if (role.deco === "flower") { const cx = 175; const cy = 22; g.appendChild( el("g", { class: "desk-deco desk-deco-flower" }, [ el("circle", { cx: cx - 7, cy, r: 6 }), el("circle", { cx: cx + 7, cy, r: 6 }), el("circle", { cx, cy: cy - 7, r: 6 }), el("circle", { cx, cy: cy + 7, r: 6 }), el("circle", { cx, cy, r: 4, class: "desk-deco-flower-center" }), ]) ); } else if (role.deco === "check") { g.appendChild( el("path", { d: "M162,24 l8,8 16,-18", class: "desk-deco desk-deco-check" }) ); } g.appendChild( el("circle", { cx: DESK.w / 2, cy: 40 + DESK.h + 30 - 34, r: 16, class: "person-head" }) ); g.appendChild( el("rect", { x: DESK.w / 2 - 22, y: 40 + DESK.h + 30 - 20, width: 44, height: 34, rx: 12, class: "person-body", style: `--role-color: ${role.accentColor}`, }) ); g.appendChild( text(agent.name, { x: DESK.w / 2, y: 40 + DESK.h + 74, class: "desk-name", "text-anchor": "middle", }) ); } else { g.appendChild( text("frei", { x: DESK.w / 2, y: 40 + DESK.h + 74, class: "desk-name desk-name-empty", "text-anchor": "middle", }) ); } return g; } function renderDesks(agents) { const g = el("g", { class: "desks" }); buildDeskLayout(agents).forEach((desk) => g.appendChild(renderDesk(desk))); return g; } export function renderOfficeScene(container, data = {}) { const { ticketCounts = DEMO_TICKET_COUNTS, agents = DEMO_AGENTS, systems = DEMO_SYSTEMS, events = DEMO_EVENTS, } = data; const svg = el("svg", { viewBox: `0 0 ${VIEW_W} ${VIEW_H}`, class: "office-svg", role: "img", "aria-label": "Buero-Visualisierung mit Kanban-Board, Monitor-Wand und Schreibtischen", }); svg.appendChild(renderRoom()); svg.appendChild(renderBoard(ticketCounts)); svg.appendChild(renderMonitorWall(systems, events)); svg.appendChild(renderDesks(agents)); container.replaceChildren(svg); } document.addEventListener("DOMContentLoaded", () => { const container = document.getElementById("office-scene"); if (container) { renderOfficeScene(container); } });