Tickets auf dem Board in der Farbe ihres Projekts zeichnen
Das Projekte-Modal am PO laesst seit #27 je Projekt eine Farbe pflegen, und GET /api/v1/tickets liefert sie pro Ticket mit (ticket.color) - auf dem Board kam sie aber nie an: renderBoard() bekam nur Zaehler je Spalte, jeder Zettel war derselbe gelbe Standardzettel. buildBoardColumnLayout() bekommt deshalb zusaetzlich die gruppierten Tickets und liefert die Farbe je Zettel mit. Ohne Ticketliste (Demo-Modus, der nur Zaehler kennt) bleibt alles wie bisher. Ein Stapel steht fuer mehr Tickets als er Zettel hat und zeigt die ersten verschiedenen Farben darin - so ist auch an "Done" (immer ein Stapel) zu sehen, aus welchen Projekten die Tickets stammen. Reichen die Farben nicht fuer alle drei Zettel, werden sie zyklisch wiederholt: ein ungefaerbter Rest-Zettel saehe sonst wie ein weiteres Projekt aus, denn --color-note ist von einem gelben Projekt kaum zu unterscheiden. Gefaerbte Zettel bekommen ausserdem eine neutrale statt der gelben Kontur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d5f7ac9f92
commit
ebf7dba537
4 changed files with 178 additions and 28 deletions
|
|
@ -196,6 +196,13 @@ header.office-header h1 {
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Zettel in der Farbe seines Projekts (Issue #27): die Fuellung kommt als Inline-Style aus
|
||||||
|
der Projektkonfiguration, die Kontur muss dann neutral sein - die gelbe Standardkontur
|
||||||
|
passt nur zum gelben Standardzettel. */
|
||||||
|
.note-colored {
|
||||||
|
stroke: rgba(15, 23, 42, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
.note-stack-badge {
|
.note-stack-badge {
|
||||||
fill: var(--color-note-stack-badge);
|
fill: var(--color-note-stack-badge);
|
||||||
stroke: #92400e;
|
stroke: #92400e;
|
||||||
|
|
|
||||||
|
|
@ -150,18 +150,35 @@ function renderCoffeeCorner() {
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStackBadge(x, y, count) {
|
// Faerbt einen Zettel in der Farbe seines Quell-Projekts (Issue #26/#27: "welcher Farbe
|
||||||
|
// diese zugeordnet werden"). Ohne Farbe - Ticket ohne Projekt oder Demo-Modus - bleibt es
|
||||||
|
// bei der Standardfarbe aus dem Stylesheet. Die Farbe ist Nutzereingabe aus dem
|
||||||
|
// Projekte-Modal und landet deshalb ueber die style-Property im DOM, nie in einem
|
||||||
|
// zusammengesetzten Attribut-String.
|
||||||
|
function paintNote(rect, color) {
|
||||||
|
if (color) {
|
||||||
|
rect.style.fill = color;
|
||||||
|
// Die gelbe Standardkontur passt nur zum gelben Standardzettel - siehe .note-colored.
|
||||||
|
rect.classList.add("note-colored");
|
||||||
|
}
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStackBadge(x, y, count, stackColors = []) {
|
||||||
const g = el("g", { class: "note-stack" });
|
const g = el("g", { class: "note-stack" });
|
||||||
[6, 3, 0].forEach((offset, i) => {
|
[6, 3, 0].forEach((offset, i) => {
|
||||||
g.appendChild(
|
g.appendChild(
|
||||||
el("rect", {
|
paintNote(
|
||||||
x: x - offset,
|
el("rect", {
|
||||||
y: y - offset,
|
x: x - offset,
|
||||||
width: NOTE_W,
|
y: y - offset,
|
||||||
height: NOTE_H,
|
width: NOTE_W,
|
||||||
rx: 6,
|
height: NOTE_H,
|
||||||
class: `note note-stack-layer-${i}`,
|
rx: 6,
|
||||||
})
|
class: `note note-stack-layer-${i}`,
|
||||||
|
}),
|
||||||
|
stackColors[i]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const cx = x + NOTE_W / 2;
|
const cx = x + NOTE_W / 2;
|
||||||
|
|
@ -175,10 +192,11 @@ function renderStackBadge(x, y, count) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Record<string, number>} ticketCounts
|
* @param {Record<string, number>} ticketCounts
|
||||||
|
* @param {Record<string, Array<object>>} groupedTickets - liefert die Projektfarbe je Zettel.
|
||||||
* @param {(columnKey: string) => void} onColumnClick
|
* @param {(columnKey: string) => void} onColumnClick
|
||||||
*/
|
*/
|
||||||
function renderBoard(ticketCounts, onColumnClick) {
|
function renderBoard(ticketCounts, groupedTickets, onColumnClick) {
|
||||||
const columns = buildBoardColumnLayout(ticketCounts);
|
const columns = buildBoardColumnLayout(ticketCounts, groupedTickets);
|
||||||
const g = el("g", { class: "board" });
|
const g = el("g", { class: "board" });
|
||||||
g.appendChild(el("rect", { ...BOARD_FRAME, rx: 10, class: "board-frame" }));
|
g.appendChild(el("rect", { ...BOARD_FRAME, rx: 10, class: "board-frame" }));
|
||||||
g.appendChild(
|
g.appendChild(
|
||||||
|
|
@ -227,13 +245,18 @@ function renderBoard(ticketCounts, onColumnClick) {
|
||||||
|
|
||||||
for (let i = 0; i < column.visibleNotes; i++) {
|
for (let i = 0; i < column.visibleNotes; i++) {
|
||||||
colGroup.appendChild(
|
colGroup.appendChild(
|
||||||
el("rect", { x: noteX, y: noteY, width: NOTE_W, height: NOTE_H, rx: 6, class: "note" })
|
paintNote(
|
||||||
|
el("rect", { x: noteX, y: noteY, width: NOTE_W, height: NOTE_H, rx: 6, class: "note" }),
|
||||||
|
column.noteColors[i]
|
||||||
|
)
|
||||||
);
|
);
|
||||||
noteY += NOTE_STEP_Y;
|
noteY += NOTE_STEP_Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (column.stackCount > 0) {
|
if (column.stackCount > 0) {
|
||||||
colGroup.appendChild(renderStackBadge(noteX, noteY, column.stackCount));
|
colGroup.appendChild(
|
||||||
|
renderStackBadge(noteX, noteY, column.stackCount, column.stackColors)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
g.appendChild(colGroup);
|
g.appendChild(colGroup);
|
||||||
|
|
@ -1380,7 +1403,7 @@ export function renderOfficeScene(container, data = {}) {
|
||||||
|
|
||||||
const groupedTickets = groupTicketsByColumn(tickets);
|
const groupedTickets = groupTicketsByColumn(tickets);
|
||||||
|
|
||||||
const boardGroup = renderBoard(ticketCounts, (columnKey) => {
|
const boardGroup = renderBoard(ticketCounts, groupedTickets, (columnKey) => {
|
||||||
openModal(
|
openModal(
|
||||||
buildTicketModalContent(columnKey, groupedTickets[columnKey] || []),
|
buildTicketModalContent(columnKey, groupedTickets[columnKey] || []),
|
||||||
renderTicketModalBody
|
renderTicketModalBody
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ export const MAX_SYSTEM_TILES = 9;
|
||||||
export const MAX_EVENT_LINES = 5;
|
export const MAX_EVENT_LINES = 5;
|
||||||
export const DEFAULT_DESK_COUNT = 8;
|
export const DEFAULT_DESK_COUNT = 8;
|
||||||
|
|
||||||
|
// Ein Stapel wird aus so vielen versetzten Zetteln gezeichnet (siehe renderStackBadge()) -
|
||||||
|
// entsprechend viele Projektfarben kann er zeigen.
|
||||||
|
export const NOTE_STACK_LAYERS = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Baut aus rohen Ticket-Zaehlern pro Spalte die Darstellung fuer das Board.
|
* Baut aus rohen Ticket-Zaehlern pro Spalte die Darstellung fuer das Board.
|
||||||
* Regel (Detailantworten F3): mehr als 5 Tickets in einer Spalte -> die aeltesten 4 werden
|
* Regel (Detailantworten F3): mehr als 5 Tickets in einer Spalte -> die aeltesten 4 werden
|
||||||
|
|
@ -33,31 +37,72 @@ export const DEFAULT_DESK_COUNT = 8;
|
||||||
* Regel (Detailantworten F2): die Spalte "done" wird immer als ein Stapel (Zeit-Historie)
|
* Regel (Detailantworten F2): die Spalte "done" wird immer als ein Stapel (Zeit-Historie)
|
||||||
* dargestellt, unabhaengig von der Anzahl.
|
* dargestellt, unabhaengig von der Anzahl.
|
||||||
*
|
*
|
||||||
|
* Die Farben stammen aus dem Quell-Projekt des Tickets (ticket.color, gepflegt im
|
||||||
|
* Projekte-Modal am PO, Issue #27) und sind optional: ohne groupedTickets - etwa im
|
||||||
|
* Demo-Modus, der nur Zaehler kennt - bleiben die Zettel einfarbig wie zuvor.
|
||||||
|
*
|
||||||
* @param {Record<string, number>} ticketCounts - Anzahl Tickets je Spalten-Key.
|
* @param {Record<string, number>} ticketCounts - Anzahl Tickets je Spalten-Key.
|
||||||
* @returns {Array<{key: string, label: string, count: number, visibleNotes: number, stackCount: number}>}
|
* @param {Record<string, Array<object>>} groupedTickets - Tickets je Spalte in derselben
|
||||||
|
* Reihenfolge wie gezaehlt (siehe groupTicketsByColumn()).
|
||||||
|
* @returns {Array<{key: string, label: string, count: number, visibleNotes: number,
|
||||||
|
* stackCount: number, noteColors: Array<string|null>, stackColors: Array<string>}>}
|
||||||
*/
|
*/
|
||||||
export function buildBoardColumnLayout(ticketCounts = {}) {
|
export function buildBoardColumnLayout(ticketCounts = {}, groupedTickets = {}) {
|
||||||
return BOARD_COLUMNS.map(({ key, label }) => {
|
return BOARD_COLUMNS.map(({ key, label }) => {
|
||||||
const count = Math.max(0, ticketCounts[key] ?? 0);
|
const count = Math.max(0, ticketCounts[key] ?? 0);
|
||||||
|
const tickets = groupedTickets[key] ?? [];
|
||||||
|
|
||||||
if (key === "done") {
|
const withColors = (visibleNotes, stackCount) => ({
|
||||||
return { key, label, count, visibleNotes: 0, stackCount: count };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count <= MAX_VISIBLE_NOTES_PER_COLUMN + 1) {
|
|
||||||
return { key, label, count, visibleNotes: count, stackCount: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
key,
|
||||||
label,
|
label,
|
||||||
count,
|
count,
|
||||||
visibleNotes: MAX_VISIBLE_NOTES_PER_COLUMN,
|
visibleNotes,
|
||||||
stackCount: count - MAX_VISIBLE_NOTES_PER_COLUMN,
|
stackCount,
|
||||||
};
|
noteColors: noteColors(tickets.slice(0, visibleNotes), visibleNotes),
|
||||||
|
stackColors: stackCount > 0 ? stackLayerColors(tickets.slice(visibleNotes)) : [],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (key === "done") {
|
||||||
|
return withColors(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count <= MAX_VISIBLE_NOTES_PER_COLUMN + 1) {
|
||||||
|
return withColors(count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return withColors(MAX_VISIBLE_NOTES_PER_COLUMN, count - MAX_VISIBLE_NOTES_PER_COLUMN);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// null steht fuer "keine Projektfarbe" (Ticket ohne Quell-Projekt, z.B. per API gemeldet) -
|
||||||
|
// solche Zettel behalten die Standardfarbe aus dem Stylesheet.
|
||||||
|
function noteColors(tickets, length) {
|
||||||
|
return Array.from({ length }, (_, i) => tickets[i]?.color ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ein Stapel steht fuer viele Tickets, aber nur fuer NOTE_STACK_LAYERS Zettel: er zeigt
|
||||||
|
// deshalb die ersten verschiedenen Projektfarben darin, statt eine davon dreimal zu
|
||||||
|
// wiederholen. So ist auch an "Done" (immer ein Stapel) zu sehen, aus welchen Projekten die
|
||||||
|
// Tickets stammen.
|
||||||
|
//
|
||||||
|
// Reichen die gefundenen Farben nicht fuer alle Zettel, werden sie zyklisch wiederholt: ein
|
||||||
|
// ungefaerbter Rest-Zettel wuerde sonst wie ein weiteres Projekt aussehen, denn die
|
||||||
|
// Standardfarbe (--color-note) ist von einem gelben Projekt kaum zu unterscheiden. Nur wenn
|
||||||
|
// gar keine Farbe vorkommt, bleibt der Stapel komplett im Standard.
|
||||||
|
function stackLayerColors(tickets) {
|
||||||
|
const found = [];
|
||||||
|
for (const ticket of tickets) {
|
||||||
|
if (ticket.color && !found.includes(ticket.color)) {
|
||||||
|
found.push(ticket.color);
|
||||||
|
if (found.length === NOTE_STACK_LAYERS) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Array.from({ length: NOTE_STACK_LAYERS }, (_, i) => found[i % found.length]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ordnet Agenten-Demo-Daten den Schreibtischen zu. Unbesetzte Schreibtische bleiben leer.
|
* Ordnet Agenten-Demo-Daten den Schreibtischen zu. Unbesetzte Schreibtische bleiben leer.
|
||||||
* Ueberzaehlige Agenten (mehr als deskCount) werden ignoriert - Phase 2 zeigt nur den
|
* Ueberzaehlige Agenten (mehr als deskCount) werden ignoriert - Phase 2 zeigt nur den
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
MAX_SYSTEM_TILES,
|
MAX_SYSTEM_TILES,
|
||||||
MAX_EVENT_LINES,
|
MAX_EVENT_LINES,
|
||||||
DEFAULT_DESK_COUNT,
|
DEFAULT_DESK_COUNT,
|
||||||
|
NOTE_STACK_LAYERS,
|
||||||
aggregateTicketsByColumn,
|
aggregateTicketsByColumn,
|
||||||
groupTicketsByColumn,
|
groupTicketsByColumn,
|
||||||
buildTicketModalContent,
|
buildTicketModalContent,
|
||||||
|
|
@ -82,6 +83,80 @@ test("buildBoardColumnLayout behandelt fehlende/negative Zaehler als 0", () => {
|
||||||
assert.equal(qa.visibleNotes, 0);
|
assert.equal(qa.visibleNotes, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("buildBoardColumnLayout faerbt jeden Zettel in der Farbe seines Projekts (Issue #27)", () => {
|
||||||
|
const tickets = [
|
||||||
|
{ column_name: "bereit", color: "#fbca04" },
|
||||||
|
{ column_name: "bereit", color: "#28a745" },
|
||||||
|
{ column_name: "bereit", color: "#fbca04" },
|
||||||
|
];
|
||||||
|
const bereit = buildBoardColumnLayout(
|
||||||
|
aggregateTicketsByColumn(tickets),
|
||||||
|
groupTicketsByColumn(tickets)
|
||||||
|
).find((c) => c.key === "bereit");
|
||||||
|
|
||||||
|
assert.deepEqual(bereit.noteColors, ["#fbca04", "#28a745", "#fbca04"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildBoardColumnLayout laesst Zettel ohne Projektfarbe ungefaerbt", () => {
|
||||||
|
const tickets = [{ column_name: "qa" }, { column_name: "qa", color: "#0366d6" }];
|
||||||
|
const qa = buildBoardColumnLayout(
|
||||||
|
aggregateTicketsByColumn(tickets),
|
||||||
|
groupTicketsByColumn(tickets)
|
||||||
|
).find((c) => c.key === "qa");
|
||||||
|
|
||||||
|
assert.deepEqual(qa.noteColors, [null, "#0366d6"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildBoardColumnLayout ohne Ticketliste (Demo-Modus) laesst alle Zettel ungefaerbt", () => {
|
||||||
|
const bereit = buildBoardColumnLayout({ bereit: 2 }).find((c) => c.key === "bereit");
|
||||||
|
assert.deepEqual(bereit.noteColors, [null, null]);
|
||||||
|
assert.deepEqual(bereit.stackColors, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildBoardColumnLayout zeigt im Stapel die verschiedenen Projektfarben der gestapelten Tickets", () => {
|
||||||
|
// 6 Tickets: die aeltesten 4 einzeln, die restlichen 2 im Stapel - dessen Farben stammen
|
||||||
|
// ausschliesslich aus diesen beiden.
|
||||||
|
const tickets = [
|
||||||
|
...Array.from({ length: 4 }, () => ({ column_name: "backlog", color: "#fbca04" })),
|
||||||
|
{ column_name: "backlog", color: "#28a745" },
|
||||||
|
{ column_name: "backlog", color: "#0366d6" },
|
||||||
|
];
|
||||||
|
const backlog = buildBoardColumnLayout(
|
||||||
|
aggregateTicketsByColumn(tickets),
|
||||||
|
groupTicketsByColumn(tickets)
|
||||||
|
).find((c) => c.key === "backlog");
|
||||||
|
|
||||||
|
assert.equal(backlog.stackCount, 2);
|
||||||
|
// Dritter Zettel: zyklische Wiederholung statt Standardfarbe - siehe stackLayerColors().
|
||||||
|
assert.deepEqual(backlog.stackColors, ["#28a745", "#0366d6", "#28a745"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildBoardColumnLayout laesst einen Stapel ganz ohne Projektfarben im Standard", () => {
|
||||||
|
const tickets = Array.from({ length: 7 }, () => ({ column_name: "backlog" }));
|
||||||
|
const backlog = buildBoardColumnLayout(
|
||||||
|
aggregateTicketsByColumn(tickets),
|
||||||
|
groupTicketsByColumn(tickets)
|
||||||
|
).find((c) => c.key === "backlog");
|
||||||
|
|
||||||
|
assert.equal(backlog.stackCount, 3);
|
||||||
|
assert.deepEqual(backlog.stackColors, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buildBoardColumnLayout zeigt im Stapel jede Projektfarbe nur einmal und hoechstens NOTE_STACK_LAYERS", () => {
|
||||||
|
const tickets = ["#fbca04", "#fbca04", "#28a745", "#0366d6", "#ec4899"].map((color) => ({
|
||||||
|
column_name: "done",
|
||||||
|
color,
|
||||||
|
}));
|
||||||
|
const done = buildBoardColumnLayout(
|
||||||
|
aggregateTicketsByColumn(tickets),
|
||||||
|
groupTicketsByColumn(tickets)
|
||||||
|
).find((c) => c.key === "done");
|
||||||
|
|
||||||
|
assert.equal(done.stackCount, 5);
|
||||||
|
assert.equal(done.stackColors.length, NOTE_STACK_LAYERS);
|
||||||
|
assert.deepEqual(done.stackColors, ["#fbca04", "#28a745", "#0366d6"]);
|
||||||
|
});
|
||||||
|
|
||||||
test("buildDeskLayout erzeugt genau DEFAULT_DESK_COUNT Schreibtische", () => {
|
test("buildDeskLayout erzeugt genau DEFAULT_DESK_COUNT Schreibtische", () => {
|
||||||
const desks = buildDeskLayout([]);
|
const desks = buildDeskLayout([]);
|
||||||
assert.equal(desks.length, DEFAULT_DESK_COUNT);
|
assert.equal(desks.length, DEFAULT_DESK_COUNT);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue