WIP Zentrale-Kachel nach VanVans Werktisch — NOCH NICHT FERTIG
Aufbau steht (Ring links, Titel mittig, Uhr rechts), aber pruef-start-ansicht meldet 3 Befunde am MAUS-LICHT der Kacheln. Gemessen: alter Stand 0 Befunde, dieser Stand 3 — kommt also von hier. Kein JS-Fehler (pageerror/console sind still), also Zeitverhalten: Die zusaetzliche Abfrage /api/zentrale verzoegert vermutlich den Aufbau der Kacheln ueber den Zeitpunkt hinaus, an dem kopf.js lichtFolgen ruft. Ausserdem offen: 2 Schriftgroessen unter 11,5 px (pruef-css-klassen). NICHT ausliefern.
This commit is contained in:
@@ -26,6 +26,7 @@ import { reportRouter } from "./workspace-reports.js";
|
||||
import { scoutRouter } from "./workspace-scouts.js";
|
||||
import { callRouter } from "./workspace-calls.js";
|
||||
import { hinweisRouter } from "./workspace-hinweise.js";
|
||||
import { zentraleRouter } from "./workspace-zentrale.js";
|
||||
import { sucheRouter } from "./workspace-suche.js";
|
||||
import { startcheckRouter } from "./workspace-startcheck.js";
|
||||
import { wissenRouter } from "./workspace-wissen.js";
|
||||
@@ -216,6 +217,7 @@ app.use(reportRouter);
|
||||
app.use(scoutRouter);
|
||||
app.use(callRouter);
|
||||
app.use(hinweisRouter);
|
||||
app.use(zentraleRouter);
|
||||
app.use(sucheRouter);
|
||||
app.use(startcheckRouter);
|
||||
app.use(wissenRouter);
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/* =====================================================================
|
||||
workspace-zentrale.js — die Zahlen fuer den Ring auf der Startseite.
|
||||
|
||||
Wunsch Filipe (08.09.2026): die Begruessungskachel nach dem Vorbild
|
||||
von VanVans Business Hub umbauen -- grosser Ring links, Text in der
|
||||
Mitte, Uhr rechts. Dort zeigt der Ring "91 % deiner Produktakten":
|
||||
ein Segment je Produkt, eingefaerbt nach Vollstaendigkeit.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
WARUM ES DIESELBE ZAHL HIER NICHT GIBT
|
||||
|
||||
Nachgemessen an der echten Datenbank am 08.09.2026:
|
||||
|
||||
Termine 31 · Wissen 7 · Schulungen 4 · Personen 9 · AUFGABEN 1
|
||||
Leistung, Leads, Profile, Content-Saeulen, Dateien: jeweils 0
|
||||
|
||||
VanVans Kranz funktioniert, weil 253 Produkte ein BILD ergeben. Ein
|
||||
Kranz aus einer Aufgabe ergibt keins. Die naheliegende Uebertragung
|
||||
("dann eben Aufgaben") haette eine leere Scheibe erzeugt, die aussieht
|
||||
wie ein Ladefehler.
|
||||
|
||||
Filipe hat deshalb DAS TEAM gewaehlt: ein Segment je Person, gefaerbt
|
||||
danach, wie es um sie steht. Neun Segmente sind wenige, aber sie sind
|
||||
ECHT und aendern sich taeglich -- und die Zahl waechst mit dem Team.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
UND WARUM DAS NICHT FUER JEDEN GEHT
|
||||
|
||||
Ein Creator darf die Teamliste nicht sehen. Das ist keine
|
||||
Feinheit, sondern die Regel, die diesen ganzen Arbeitsplatz traegt
|
||||
(Wunsch Filipe, 07.09.2026: "jeder soll und darf im kalender immer nur
|
||||
seine eigenen eintraege nur sehen"). Ein Ring, der neun Namen
|
||||
einfaerbt, waere fuer einen Creator ein Leck -- er koennte daran
|
||||
ablesen, wer heute Termine hat und wer nicht.
|
||||
|
||||
Deshalb liefert diese Route ZWEI VERSCHIEDENE RINGE, je nach Rolle:
|
||||
|
||||
Leitung (DogFather, Manager, Scout, Spicy)
|
||||
-> das Team: ein Segment je aktiver Person
|
||||
Creator -> der eigene Tag: ein Segment je Stunde, plus die
|
||||
eigenen Termine als Marken
|
||||
|
||||
Das ist keine Notloesung, sondern die richtige Antwort auf dieselbe
|
||||
Frage: "Wie weit ist das, wofuer ich zustaendig bin?" Fuer DogFather
|
||||
ist das sein Team, fuer eine Creatorin ihr Tag.
|
||||
|
||||
KEINE EIGENEN DATEN, wie bei den Hinweisen: Alles hier ist eine SICHT
|
||||
auf Zeilen, die ohnehin existieren. Es gibt nichts zu pflegen und
|
||||
nichts, was veralten kann.
|
||||
===================================================================== */
|
||||
|
||||
import express from "express";
|
||||
import { db, sitzungLesen, istLeitung, betreuteIds } from "./workspace.js";
|
||||
|
||||
export const zentraleRouter = express.Router();
|
||||
|
||||
function angemeldet(req, res, next) {
|
||||
const person = sitzungLesen(req);
|
||||
if (!person) return res.status(401).json({ fehler: "nicht_angemeldet" });
|
||||
req.person = person;
|
||||
next();
|
||||
}
|
||||
zentraleRouter.use("/workspace/api/zentrale", angemeldet);
|
||||
|
||||
const p2 = (n) => String(n).padStart(2, "0");
|
||||
/* ORTSZEIT, NICHT UTC. toISOString() liefert zwischen Mitternacht und
|
||||
zwei Uhr noch den Vortag -- der Ring stuende dann auf den Terminen
|
||||
von gestern. Derselbe Fehler ist mir am 08.09. zweimal an einem Abend
|
||||
passiert; pruef-struktur.mjs sucht ihn inzwischen im ganzen Repo. */
|
||||
function heuteLokal() {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}`;
|
||||
}
|
||||
|
||||
zentraleRouter.get("/workspace/api/zentrale", (req, res) => {
|
||||
try {
|
||||
const d = db();
|
||||
const heute = heuteLokal();
|
||||
const ich = req.person;
|
||||
|
||||
/* Wessen Lage wird gezeigt? Bei aktiver Fremdsicht die der Person,
|
||||
in die man hineinsieht -- sonst zeigte die Kachel die eigene Lage
|
||||
und alles darunter eine fremde. Genau diese Uneinheitlichkeit hat
|
||||
auf der Startseite schon einmal zu "ist das jetzt meins oder
|
||||
ihrs?" gefuehrt. */
|
||||
const zeigt = req.person.sicht || req.person;
|
||||
|
||||
if (istLeitung(ich)) {
|
||||
/* ---------- DAS TEAM ----------
|
||||
Ein Segment je aktiver Person. Der Zustand einer Person ist
|
||||
bewusst GROB in drei Stufen -- mehr taeuschte Genauigkeit vor,
|
||||
die es nicht gibt (dieselbe Ueberlegung steht in VanVans Kranz).
|
||||
|
||||
frei - nichts Offenes, keine Termine heute -> ruhig
|
||||
dran - hat heute Termine -> die sind versorgt
|
||||
offen - hat unerledigte Termine aus der VERGANGENHEIT
|
||||
-> das ist das, was liegen bleibt
|
||||
*/
|
||||
const leute = d.prepare(
|
||||
`SELECT id, name, rolle FROM personen WHERE aktiv = 1 ORDER BY rolle, name`).all();
|
||||
|
||||
const heuteZaehler = d.prepare(
|
||||
`SELECT COUNT(*) AS n FROM termine t
|
||||
WHERE t.erledigt = 0 AND substr(t.beginn, 1, 10) = ?
|
||||
AND (t.creator_id = ? OR t.erstellt_von = ?
|
||||
OR EXISTS (SELECT 1 FROM termin_teilnehmer x
|
||||
WHERE x.termin_id = t.id AND x.person_id = ?))`);
|
||||
const altZaehler = d.prepare(
|
||||
`SELECT COUNT(*) AS n FROM termine t
|
||||
WHERE t.erledigt = 0 AND substr(t.beginn, 1, 10) < ?
|
||||
AND (t.creator_id = ? OR t.erstellt_von = ?
|
||||
OR EXISTS (SELECT 1 FROM termin_teilnehmer x
|
||||
WHERE x.termin_id = t.id AND x.person_id = ?))`);
|
||||
|
||||
const segmente = leute.map((p) => {
|
||||
const heuteN = heuteZaehler.get(heute, p.id, p.id, p.id).n;
|
||||
const altN = altZaehler.get(heute, p.id, p.id, p.id).n;
|
||||
const stufe = altN > 0 ? "offen" : (heuteN > 0 ? "dran" : "frei");
|
||||
/* NUR DER VORNAME nach aussen. Der Ring braucht ein Kuerzel fuer
|
||||
die Sprechblase, nicht den Datensatz einer Person. */
|
||||
return { name: String(p.name || "").trim().split(/\s+/)[0] || "?",
|
||||
rolle: p.rolle, stufe, heute: heuteN, alt: altN };
|
||||
});
|
||||
|
||||
const versorgt = segmente.filter((s) => s.stufe !== "offen").length;
|
||||
return res.json({
|
||||
art: "team",
|
||||
titel: "Team versorgt",
|
||||
prozent: segmente.length ? Math.round((versorgt / segmente.length) * 100) : 100,
|
||||
segmente,
|
||||
zahlen: [
|
||||
{ wert: segmente.length, schild: "im Team" },
|
||||
{ wert: segmente.filter((s) => s.stufe === "dran").length, schild: "heute dran" },
|
||||
{ wert: segmente.filter((s) => s.stufe === "offen").length, schild: "liegt liegen" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- DER EIGENE TAG ----------
|
||||
Fuer Creator. Ein Segment je Stunde von 6 bis 24 -- der Teil des
|
||||
Tages, in dem gearbeitet wird. Nachts einen leeren Halbkreis zu
|
||||
zeigen sagt nichts; 18 Segmente fuellen den Ring ordentlich.
|
||||
|
||||
Gefuellt sind die vergangenen Stunden, markiert die mit einem
|
||||
eigenen Termin. Der Ring beantwortet damit "wie weit ist mein
|
||||
Tag" -- dieselbe Frage wie bei VanVan, nur auf das bezogen,
|
||||
wofuer eine Creatorin zustaendig ist. */
|
||||
const VON = 6, BIS = 24;
|
||||
const jetzt = new Date();
|
||||
const stundeJetzt = jetzt.getHours();
|
||||
|
||||
const meine = d.prepare(
|
||||
`SELECT t.beginn FROM termine t
|
||||
WHERE substr(t.beginn, 1, 10) = ?
|
||||
AND (t.creator_id = ? OR t.erstellt_von = ?
|
||||
OR EXISTS (SELECT 1 FROM termin_teilnehmer x
|
||||
WHERE x.termin_id = t.id AND x.person_id = ?))`)
|
||||
.all(heute, zeigt.id, zeigt.id, zeigt.id);
|
||||
const mitTermin = new Set(meine.map((t) => Number(String(t.beginn).slice(11, 13))));
|
||||
|
||||
const segmente = [];
|
||||
for (let s = VON; s < BIS; s++) {
|
||||
segmente.push({ name: `${p2(s)} Uhr`, rolle: "stunde",
|
||||
stufe: mitTermin.has(s) ? "dran" : (s < stundeJetzt ? "frei" : "offen") });
|
||||
}
|
||||
const durch = Math.min(Math.max(stundeJetzt - VON, 0), BIS - VON);
|
||||
return res.json({
|
||||
art: "tag",
|
||||
titel: "Tag geschafft",
|
||||
prozent: Math.round((durch / (BIS - VON)) * 100),
|
||||
segmente,
|
||||
zahlen: [
|
||||
{ wert: meine.length, schild: "heute" },
|
||||
{ wert: [...mitTermin].filter((s) => s >= stundeJetzt).length, schild: "kommt noch" },
|
||||
{ wert: betreuteIds(zeigt).length, schild: "betreut" },
|
||||
],
|
||||
});
|
||||
} catch (fehler) {
|
||||
console.error("[workspace] Zentrale:", fehler?.message);
|
||||
res.status(503).json({ fehler: "nicht_verfuegbar" });
|
||||
}
|
||||
});
|
||||
@@ -346,11 +346,212 @@
|
||||
transparent 74%);
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
DREI SPALTEN: RING — TEXT — UHR (08.09.2026)
|
||||
|
||||
Uebernommen von VanVans Werktisch im Business Hub, auf Wunsch Filipes
|
||||
("das model da uebernehmen und es noch perfektionieren"). Vorher stand
|
||||
hier eine FLEX-Zeile: Bild, Text, Zahlen, Uhr -- alles nebeneinander,
|
||||
mit rund 300 px Leere in der Mitte. Das war eine Zeile mit einer
|
||||
Luecke, keine Tafel.
|
||||
|
||||
`minmax(0, 1fr)` FUER DIE MITTE, nicht `1fr`. Das ist im Vorbild
|
||||
ausdruecklich vermerkt, und der Grund ist nachlesenswert: `1fr` geht
|
||||
nie unter die Mindestbreite seines Inhalts. Eine nicht umbrechende
|
||||
Anrede und eine Zahlenreihe zwingen die Spalte damit auf 470 px -- auf
|
||||
einem 390-px-Schirm. Die ganze Seite laesst sich dann seitlich
|
||||
wegschieben. Mit `minmax(0, 1fr)` darf die Spalte schmaler werden als
|
||||
ihr Inhalt, und der Inhalt bricht um, statt hinauszuragen.
|
||||
|
||||
Ich habe den Fehler nicht selbst gemacht, sondern die Begruendung
|
||||
mituebernommen -- eine Loesung ohne ihren Grund haelt genau bis zum
|
||||
naechsten Umbau. */
|
||||
.willkommen__reihe {
|
||||
display: flex; align-items: center; gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 232px) minmax(0, 1fr) minmax(180px, 232px);
|
||||
gap: 26px; align-items: center;
|
||||
}
|
||||
.willkommen__text {
|
||||
min-width: 0;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 9px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- Der Ring links ------------------------------------------------
|
||||
Gleich gross wie die Uhr rechts. Im Vorbild steht dazu: "Beide Kreise
|
||||
exakt gleich gross -- sie sind ein Paar, kein Haupt- und Nebenkreis."
|
||||
Das ist der Grund, warum die Tafel ruhig wirkt: zwei gleiche Gewichte
|
||||
links und rechts, dazwischen der Text. */
|
||||
.zring {
|
||||
position: relative; aspect-ratio: 1; width: 100%; max-width: 232px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.zring__bild {
|
||||
width: 100%; height: 100%; display: block;
|
||||
/* Bei -90 Grad beginnt das erste Segment oben statt rechts -- so
|
||||
liest man einen Fortschritt, seit es Uhren gibt. */
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.zring__mitte {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 2px; pointer-events: none;
|
||||
}
|
||||
/* DAS SIEGEL IST RUND -- und das ist eine Reparatur, keine Stilfrage.
|
||||
|
||||
Erst stand hier dieselbe Maske wie in der Kopfleiste:
|
||||
-webkit-mask: url("../img/marke-husky.webp") ...
|
||||
Im Nahaufnahme-Bildschirmfoto war das Ergebnis ein KASTEN mit einem
|
||||
Husky darin, kein freigestellter Husky.
|
||||
|
||||
Nachgemessen: Die Datei hat einen Alphakanal (VP8X mit ALPH-Chunk,
|
||||
Alpha-Flag gesetzt) -- er ist nur RECHTECKIG. Das Bild ist nicht
|
||||
freigestellt. Als Maske taugt es damit nicht, egal wie richtig die
|
||||
CSS-Zeile ist.
|
||||
|
||||
BEFUND FUER SPAETER: Dasselbe gilt fuer `.kopfleiste .marke::before`
|
||||
in start.css -- dort ist es bei 22 px nur nicht aufgefallen. Wer das
|
||||
Logo einmal freistellt, repariert beide Stellen auf einmal.
|
||||
|
||||
Hier wird es deshalb als BILD gezeigt und rund beschnitten, mit
|
||||
einem feinen Ring darum. Das ist naeher am Vorbild als die Maske es
|
||||
je war: VanVans Werktisch traegt in der Mitte ein rundes Siegel, kein
|
||||
ausgeschnittenes Zeichen. */
|
||||
.zring__siegel {
|
||||
width: 44px; height: 44px; margin-bottom: 5px;
|
||||
border-radius: 50%;
|
||||
background: url("../img/marke-husky.webp") center / 128% no-repeat, #0b1420;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(180, 210, 240, .28),
|
||||
inset 0 0 12px rgba(0, 0, 0, .55);
|
||||
/* Etwas entsaettigt und abgedunkelt: Das Siegel ist der ruhige Kern
|
||||
des Rings, nicht sein hellster Punkt -- sonst zieht es den Blick von
|
||||
der Zahl darunter ab, um die es geht. */
|
||||
filter: saturate(.55) brightness(.92);
|
||||
}
|
||||
.zring__prozent {
|
||||
font-size: 1.85rem; font-weight: 700; line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #dfeaf6;
|
||||
text-shadow: 0 0 16px rgba(127, 196, 242, .3);
|
||||
}
|
||||
/* ZWEIZEILIG, UND DAS IST GEWOLLT.
|
||||
|
||||
Erst stand hier `max-width: 78%` bei .68rem. In der Nahaufnahme lief
|
||||
"DES TEAMS VERSORGT" als eine Zeile quer ueber die Segmentbahn -- der
|
||||
Ring war dahinter durchgestrichen.
|
||||
|
||||
Der innere Segmentring liegt bei Radius 106 von 150, also auf 71 % der
|
||||
Breite. Alles darunter muss schmaler bleiben als das, sonst kreuzt es
|
||||
ihn. 58 % lassen dem Text Luft und zwingen ihn in zwei Zeilen -- genau
|
||||
wie im Vorbild, wo "DEINER PRODUKTAKTEN" ebenfalls umbricht.
|
||||
|
||||
.72rem statt .68rem, weil 10,9 px unter meiner eigenen Untergrenze von
|
||||
11,5 px lagen. Die Regel gilt auch fuer Text, den man nur ueberfliegt. */
|
||||
.zring__unter {
|
||||
font-size: .72rem; letter-spacing: .06em; text-transform: uppercase;
|
||||
color: rgba(168, 192, 218, .66);
|
||||
max-width: 58%; text-align: center; line-height: 1.25;
|
||||
text-wrap: balance; /* zwei gleich lange Zeilen statt 4+1 Woertern */
|
||||
}
|
||||
|
||||
/* ---- Die Anrede in eigener Fassung -------------------------------- */
|
||||
.zgruss {
|
||||
display: inline-flex; align-items: center; gap: 11px;
|
||||
padding: 7px 15px 7px 8px;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(178deg, rgba(10, 18, 30, .62), rgba(6, 12, 21, .48));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(160, 195, 230, .13),
|
||||
0 1px 10px rgba(0, 0, 0, .3);
|
||||
max-width: 100%;
|
||||
}
|
||||
.zgruss__bild {
|
||||
width: 42px; height: 42px; flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background-size: cover; background-position: center;
|
||||
background-color: rgba(120, 160, 200, .16);
|
||||
box-shadow: 0 0 0 1px rgba(180, 210, 240, .22), inset 0 0 14px rgba(0, 0, 0, .5);
|
||||
display: grid; place-items: center;
|
||||
font-weight: 700; color: #cfe2f5;
|
||||
}
|
||||
.zgruss__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; text-align: left; }
|
||||
.zgruss__datum { font-size: .78rem; color: rgba(168, 192, 218, .72); }
|
||||
.zgruss__rolle {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px 11px; border-radius: 999px;
|
||||
font-size: .66rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
|
||||
color: #e6c88a;
|
||||
border: 1px solid rgba(227, 179, 65, .34);
|
||||
background: rgba(227, 179, 65, .09);
|
||||
}
|
||||
|
||||
/* ---- Zierzeile und Titel ------------------------------------------ */
|
||||
.zmarke {
|
||||
display: inline-flex; align-items: center; gap: 11px;
|
||||
font-size: .68rem; font-weight: 600; letter-spacing: .24em; text-transform: uppercase;
|
||||
color: rgba(196, 214, 234, .6);
|
||||
margin-top: 3px;
|
||||
}
|
||||
/* Die Raute ist ein ELEMENT, kein Schriftzeichen. Ein ◆ aus der
|
||||
Schriftart sitzt nie auf der Grundlinie und sieht auf jedem Geraet
|
||||
anders aus -- im Vorbild steht dieselbe Ueberlegung. */
|
||||
.zraute {
|
||||
width: 5px; height: 5px; flex: 0 0 auto;
|
||||
transform: rotate(45deg);
|
||||
background: rgba(227, 179, 65, .62);
|
||||
}
|
||||
/* ZWEI STUFEN, wie im Vorbild -- nicht eine.
|
||||
|
||||
Bei mittlerer Breite fehlt zuerst der Platz fuer DREI Spalten, aber
|
||||
noch lange nicht fuer zwei: Der Ring bleibt links neben dem Text, die
|
||||
Uhr rutscht darunter und in die Mitte. Erst wenn auch das nicht mehr
|
||||
reicht, steht alles untereinander.
|
||||
|
||||
Die Zahlen 1180 und 780 sind aus dem Vorbild uebernommen, wo sie an
|
||||
echten Geraeten entstanden sind. Sie stehen hier zusammen mit dem,
|
||||
was sie zaehlen -- wer eine vierte Spalte hinzufuegt, muss sie neu
|
||||
messen. Genau das ist im September einmal vergessen worden, und der
|
||||
Sicht-Umschalter lag danach ueber dem Chat-Knopf. */
|
||||
@media (max-width: 1180px) {
|
||||
.willkommen__reihe { grid-template-columns: minmax(170px, 220px) minmax(0, 1fr); }
|
||||
.willkommen__rechts { grid-column: 1 / -1; justify-content: center; margin-left: 0; }
|
||||
}
|
||||
@media (max-width: 780px) {
|
||||
.willkommen__reihe { grid-template-columns: minmax(0, 1fr); gap: 20px; }
|
||||
.zring { max-width: 190px; }
|
||||
.zgruss { flex-wrap: wrap; justify-content: center; }
|
||||
.zgruss__text { text-align: center; align-items: center; }
|
||||
}
|
||||
|
||||
/* Der Satz unter dem Titel bekommt Fliesstextgroesse und darf umbrechen.
|
||||
|
||||
Im Vorbild stehen dort drei Zeilen ("Ich habe deine 253 Produkte
|
||||
durchgesehen und 1561 von 1720 Angaben schon eingetragen ..."), und
|
||||
genau die machen aus einer Anzeigetafel eine Ansprache. Bei mir ist
|
||||
der Satz kuerzer -- er kommt aus derselben Zaehlung wie die Hinweise
|
||||
darunter und wird nicht zweimal gerechnet -- aber er soll denselben
|
||||
Raum bekommen statt als Kleingedrucktes am Titel zu kleben.
|
||||
|
||||
`text-wrap: balance` verteilt die Zeilen gleichmaessig, statt die
|
||||
letzte mit einem einzelnen Wort enden zu lassen. */
|
||||
.willkommen__reihe .unterzeile {
|
||||
font-size: 1rem; line-height: 1.55;
|
||||
max-width: 46ch; margin: 2px auto 0;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.ztitel {
|
||||
margin: 0;
|
||||
font-size: clamp(1.9rem, 3.6vw, 2.9rem);
|
||||
font-weight: 700; line-height: 1.06; letter-spacing: -.01em;
|
||||
color: #dce9f6;
|
||||
/* Ein Verlauf statt einer Volltonfarbe: Der Titel bekommt oben Licht
|
||||
und unten Schatten, wie ein gefraester Schriftzug auf Metall. */
|
||||
background: linear-gradient(178deg, #eef5fc 8%, #b9cee2 58%, #7f95ad 100%);
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.willkommen__text { min-width: 0; flex: 1 1 260px; }
|
||||
|
||||
/* Rechts: Glocke neben der Uhr, beide rechtsbündig. `margin-left: auto`
|
||||
statt `justify-content: space-between` am Elternteil -- so bleibt die
|
||||
|
||||
@@ -505,8 +505,37 @@
|
||||
$('rollentext').textContent = ich.sicht
|
||||
? `Arbeitsplatz von ${ich.sicht.name} · ${ROLLENTEXT[zeigt.rolle] || zeigt.rolle}`
|
||||
: (ROLLENTEXT[ich.rolle] || ich.rolle);
|
||||
|
||||
/* Datum und Rolle in der Anrede -- wie im Vorbild, wo unter "Noch
|
||||
wach, Dogi" das Datum steht und daneben "ADMIN & PARTNER".
|
||||
|
||||
Das Datum steht damit ZWEIMAL auf der Kachel: oben in der
|
||||
Zierzeile mit KW, hier ohne. Das ist Absicht und keine Doppelung:
|
||||
Oben gehoert es zur Tafel ("welcher Tag ist heute"), hier zur
|
||||
Anrede ("wann begruesse ich dich"). Im Vorbild ist es genauso,
|
||||
und es liest sich dort natuerlich. Faellt es Filipe als Doppelung
|
||||
auf, kommt die Zierzeile oben weg -- nicht diese Zeile. */
|
||||
const jetzt2 = new Date();
|
||||
const dat = $('zgruss-datum');
|
||||
if (dat) {
|
||||
dat.textContent = `${WOCHENTAGE[jetzt2.getDay()]}, `
|
||||
+ `${jetzt2.getDate()}. ${MONATE[jetzt2.getMonth()]}`;
|
||||
}
|
||||
const rolleEl = $('zgruss-rolle');
|
||||
if (rolleEl) {
|
||||
rolleEl.textContent = (ROLLEN_KURZ[zeigt.rolle] || zeigt.rolle || '').toUpperCase();
|
||||
rolleEl.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Kurzform der Rolle fuer die Plakette neben dem Namen. Der lange
|
||||
Text ("Gesamtuebersicht und Freigaben") steht schon darunter --
|
||||
hier braucht es das Wort, nicht die Erklaerung. */
|
||||
const ROLLEN_KURZ = {
|
||||
admin: 'DogFather', manager: 'Manager', scout: 'Scout',
|
||||
creator: 'Creator', spicy: 'Spicy Media',
|
||||
};
|
||||
|
||||
/* Ein Satz zur Lage -- gerechnet aus denselben Hinweisen, die darunter
|
||||
stehen. Keine zweite Zählung, sonst gäbe es irgendwann zwei
|
||||
Wahrheiten auf einem Bildschirm. */
|
||||
@@ -534,6 +563,145 @@
|
||||
standSetzen('offen', gesamt === 0 ? '0' : String(gesamt), stufe);
|
||||
}
|
||||
|
||||
/* ---------- Der Ring links ----------------------------------------------
|
||||
Nach dem Vorbild von VanVans Werktisch. Zwei Bahnen:
|
||||
|
||||
INNEN eine feine Bahn mit EINEM Segment je Person (bzw. je
|
||||
Stunde), eingefaerbt nach Lage -- dort sieht man jedes
|
||||
Einzelne.
|
||||
AUSSEN ein durchgehender Bogen, der GENAU auf der Prozentzahl in
|
||||
der Mitte steht.
|
||||
|
||||
Die Trennung ist aus dem Vorbild uebernommen, samt Begruendung:
|
||||
Dort zeigte der Ring erst die 162 Produkte je einzeln, und der
|
||||
Anteil des Blauen entsprach dann der Zahl der fertigen AKTEN (44 %),
|
||||
waehrend daneben die Zahl der erfuellten ANGABEN (91 %) stand. Zwei
|
||||
Auskuenfte, die sich widersprechen, sind schlimmer als eine.
|
||||
Deshalb: der Bogen sagt dasselbe wie die Zahl, immer. */
|
||||
|
||||
const RING_FARBE = {
|
||||
frei: '#57b89b', /* nichts offen -- gedaempftes Petrol, kein Grasgruen */
|
||||
dran: '#7fc4f2', /* heute versorgt */
|
||||
offen: '#ff6b74', /* liegt liegen */
|
||||
};
|
||||
|
||||
function bogen(cx, cy, r, von, bis) {
|
||||
const x1 = cx + r * Math.cos(von), y1 = cy + r * Math.sin(von);
|
||||
const x2 = cx + r * Math.cos(bis), y2 = cy + r * Math.sin(bis);
|
||||
const gross = bis - von > Math.PI ? 1 : 0;
|
||||
return `M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${r} ${r} 0 ${gross} 1 ${x2.toFixed(2)} ${y2.toFixed(2)}`;
|
||||
}
|
||||
const svgEl = (name, attr) => {
|
||||
const n = document.createElementNS('http://www.w3.org/2000/svg', name);
|
||||
for (const [k, v] of Object.entries(attr)) n.setAttribute(k, v);
|
||||
return n;
|
||||
};
|
||||
|
||||
function ringZeichnen(daten) {
|
||||
const svg = $('zring-bild');
|
||||
if (!svg) return;
|
||||
svg.replaceChildren();
|
||||
const segmente = daten.segmente || [];
|
||||
|
||||
/* Der Verlauf fuer den Fortschrittsbogen -- eine Reise von Rot nach
|
||||
Babyblau, dieselben zwei Farben wie in der Uhr daneben und im
|
||||
Rahmen der Kachel. Keine dritte Handschrift. */
|
||||
const defs = svgEl('defs', {});
|
||||
const lauf = svgEl('linearGradient', { id: 'zring-lauf', x1: '0', y1: '0', x2: '1', y2: '1' });
|
||||
for (const [o, f] of [['0%', '#ff5f6d'], ['46%', '#c07ad0'], ['100%', '#7fc4f2']]) {
|
||||
lauf.appendChild(svgEl('stop', { offset: o, 'stop-color': f }));
|
||||
}
|
||||
defs.appendChild(lauf);
|
||||
svg.appendChild(defs);
|
||||
|
||||
/* 1. Die leere Bahn zuerst -- damit der Ring auch bei 0 % ein Ring
|
||||
ist und nicht ein Punkt. */
|
||||
svg.appendChild(svgEl('path', {
|
||||
d: bogen(150, 150, 128, 0, Math.PI * 1.9999),
|
||||
stroke: 'rgba(120, 150, 185, .16)', 'stroke-width': '9',
|
||||
fill: 'none', 'stroke-linecap': 'round',
|
||||
}));
|
||||
|
||||
/* 2. Ein Segment je Person (oder Stunde), innen. Mit Luecke
|
||||
dazwischen, sonst verschmelzen sie zu einem Ring und man
|
||||
zaehlt nichts mehr. */
|
||||
if (segmente.length) {
|
||||
const schritt = (Math.PI * 2) / segmente.length;
|
||||
const luecke = Math.min(schritt * 0.22, 0.06);
|
||||
segmente.forEach((s, i) => {
|
||||
const von = i * schritt + luecke / 2;
|
||||
const bis = (i + 1) * schritt - luecke / 2;
|
||||
const st = svgEl('path', {
|
||||
d: bogen(150, 150, 106, von, bis),
|
||||
stroke: RING_FARBE[s.stufe] || '#8fb3cf',
|
||||
'stroke-width': '7', fill: 'none', 'stroke-linecap': 'round',
|
||||
opacity: s.stufe === 'offen' ? '.95' : '.72',
|
||||
});
|
||||
/* Der Name als Sprechblase. Nur der Vorname, und nur das, was
|
||||
der Server ohnehin geschickt hat -- der Browser erfaehrt
|
||||
nichts, was er nicht sehen darf. */
|
||||
const titel = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
||||
titel.textContent = s.name || '';
|
||||
st.appendChild(titel);
|
||||
svg.appendChild(st);
|
||||
});
|
||||
}
|
||||
|
||||
/* 3. Der Fortschrittsbogen aussen, genau auf der Prozentzahl. */
|
||||
const anteil = Math.max(0, Math.min(100, Number(daten.prozent) || 0));
|
||||
const bis = (anteil / 100) * Math.PI * 2;
|
||||
if (bis > 0.01) {
|
||||
const d = bogen(150, 150, 128, 0, Math.min(bis, Math.PI * 1.9999));
|
||||
svg.appendChild(svgEl('path', {
|
||||
d, stroke: 'url(#zring-lauf)', 'stroke-width': '9',
|
||||
fill: 'none', 'stroke-linecap': 'round',
|
||||
}));
|
||||
/* Ein heller Punkt an der Spitze macht aus dem Bogen einen
|
||||
Zeiger -- er markiert den Stand so genau, wie die Zahl es sagt. */
|
||||
const px = 150 + 128 * Math.cos(bis), py = 150 + 128 * Math.sin(bis);
|
||||
svg.appendChild(svgEl('circle', { cx: px, cy: py, r: '3.4', fill: '#eaf4ff' }));
|
||||
}
|
||||
|
||||
$('zring-prozent').textContent = `${anteil} %`;
|
||||
$('zring-unter').textContent = daten.titel || '';
|
||||
}
|
||||
|
||||
async function zentraleLaden() {
|
||||
let daten;
|
||||
try {
|
||||
const antwort = await fetch('/workspace/api/zentrale', { credentials: 'same-origin' });
|
||||
if (!antwort.ok) return;
|
||||
daten = await antwort.json();
|
||||
} catch { return; } /* Der Rest der Kachel steht trotzdem. */
|
||||
|
||||
ringZeichnen(daten);
|
||||
|
||||
/* Die drei Zahlen unter dem Titel kommen bei Leitung aus derselben
|
||||
Antwort -- eine Abfrage, eine Wahrheit. Fuer die Tagesansicht
|
||||
fuellen `lageZeigen` und `tagestermineLaden` sie weiter; deshalb
|
||||
wird hier nur ueberschrieben, was der Server auch geschickt hat. */
|
||||
const zahlen = daten.zahlen || [];
|
||||
const felder = ['heute', 'naechster', 'offen'];
|
||||
zahlen.slice(0, 3).forEach((z, i) => {
|
||||
const wert = $('stand-' + felder[i]);
|
||||
const schild = wert?.parentElement?.querySelector('.stand__schild');
|
||||
if (!wert || !schild) return;
|
||||
wert.textContent = String(z.wert);
|
||||
schild.textContent = z.schild;
|
||||
/* Die Farbe folgt der Bedeutung, nicht der Position: Was liegen
|
||||
bleibt, ist rot -- egal an welcher Stelle es steht. */
|
||||
const kasten = wert.closest('.stand');
|
||||
if (kasten) {
|
||||
kasten.dataset.stufe = /liegt|offen|fehlt/i.test(z.schild)
|
||||
? (Number(z.wert) > 0 ? 'warnung' : 'ruhig') : '';
|
||||
}
|
||||
});
|
||||
if (zahlen.length) {
|
||||
standVonZentrale = true;
|
||||
$('tagesstand').hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Die drei Ablesungen in der Konsolenmitte --------------------- */
|
||||
|
||||
/* EINE Funktion fuer alle drei, und sie wird von den STELLEN gerufen,
|
||||
@@ -546,7 +714,22 @@
|
||||
auf beide: Wer nichts Offenes hat, aber drei Calls, soll seinen Tag
|
||||
sehen, auch wenn die andere Abfrage haengt. Genau diesen Fehler hat
|
||||
die Kachel darunter schon einmal gemacht. */
|
||||
/* WER DIE FELDER BESETZT HAT, BEHAELT SIE (08.09.2026).
|
||||
|
||||
Seit der Ring da ist, schreiben ZWEI Quellen in dieselben drei
|
||||
Felder: `zentraleLaden` (bei Leitung: im Team / heute dran / liegt
|
||||
liegen) und `lageZeigen` + `tagestermineLaden` (Heute / Als
|
||||
Naechstes / Offen). Beide laufen nebeneinander los -- wer zuletzt
|
||||
ankommt, haette gewonnen, und das waere bei jedem Laden eine andere.
|
||||
|
||||
Ein Bildschirm, auf dem "heute dran: 2" steht und beim naechsten
|
||||
Aufruf "Heute: 5", ist schlimmer als einer, der weniger zeigt.
|
||||
Deshalb: Hat die Zentrale die Felder belegt, ruehrt sie niemand
|
||||
mehr an. Die Reihenfolge des Eintreffens spielt dann keine Rolle. */
|
||||
let standVonZentrale = false;
|
||||
|
||||
function standSetzen(welches, wert, stufe) {
|
||||
if (standVonZentrale) return;
|
||||
const feld = $('stand-' + welches);
|
||||
if (!feld) return;
|
||||
feld.textContent = wert;
|
||||
@@ -579,8 +762,26 @@
|
||||
Die Tagestermine laufen daneben her: Sie sind die rechte Hälfte
|
||||
derselben Kachel, aber niemand soll auf sie warten. Scheitern
|
||||
sie, steht die linke Hälfte trotzdem. */
|
||||
/* Der Ring laeuft daneben her, wie die Tagestermine: Er ist die
|
||||
linke Spalte der Kachel, aber niemand soll auf ihn warten.
|
||||
Scheitert er, steht der Rest trotzdem. */
|
||||
const ringLaeuft = zentraleLaden();
|
||||
|
||||
const termineLaeuft = tagestermineLaden();
|
||||
await hinweiseLaden();
|
||||
/* KEIN `await ringLaeuft` HIER (08.09.2026).
|
||||
|
||||
Genau das stand kurz an dieser Stelle, und pruef-start-ansicht
|
||||
wurde daraufhin dreimal rot -- nicht am Ring, sondern am
|
||||
MAUS-LICHT auf den Kacheln weiter unten. Der Grund: Alles, was
|
||||
nach diesem `await` kommt, wartet mit. Der Ring braucht eine
|
||||
Server-Abfrage, und in der Zeit war das Licht noch nicht
|
||||
eingehaengt, als die Pruefung mit dem Zeiger darueberfuhr.
|
||||
|
||||
Gemessen, nicht vermutet: derselbe Lauf am alten Stand meldete
|
||||
null Befunde, mit dem `await` drei. Ein zusaetzliches `await` in
|
||||
einer Ladefolge ist nie nur eine Verzoegerung fuer sich selbst --
|
||||
es verzoegert alles dahinter. */
|
||||
await termineLaeuft;
|
||||
/* Erst jetzt steht fest, wie die Lage ist. */
|
||||
lageZeigen();
|
||||
|
||||
+77
-20
@@ -47,33 +47,89 @@
|
||||
<span class="willkommen__marker" id="tagesmarker" hidden></span>
|
||||
</p>
|
||||
<div class="willkommen__reihe">
|
||||
<span class="willkommen__plakette" id="plakette" aria-hidden="true"></span>
|
||||
|
||||
<!-- ==== LINKS: DER RING ======================================
|
||||
Nach dem Vorbild von VanVans Werktisch (Business Hub), auf
|
||||
Wunsch Filipes uebernommen und weitergebaut.
|
||||
|
||||
Dort steht "91 % deiner Produktakten" -- ein Segment je
|
||||
Produkt. Diese Zahl gibt es hier nicht: Nachgemessen hat der
|
||||
Workspace 31 Termine, 9 Personen und GENAU EINE Aufgabe. Ein
|
||||
Kranz aus einer Aufgabe ist kein Bild, sondern sieht aus wie
|
||||
ein Ladefehler.
|
||||
|
||||
Filipe hat DAS TEAM gewaehlt. Fuer Creator zeigt derselbe
|
||||
Ring den eigenen Tag -- nicht als Notloesung, sondern weil
|
||||
ein Creator die Teamliste nicht sehen darf und die Frage
|
||||
dieselbe bleibt: "Wie weit ist das, wofuer ich zustaendig
|
||||
bin?" Die Entscheidung faellt im Server
|
||||
(workspace-zentrale.js), nicht hier: Was der Browser nicht
|
||||
bekommt, kann er auch nicht verraten. -->
|
||||
<div class="zring" id="zring">
|
||||
<svg class="zring__bild" id="zring-bild" viewBox="0 0 300 300"
|
||||
role="img" aria-labelledby="zring-titel zring-text">
|
||||
<title id="zring-titel">Der Stand deiner Zentrale</title>
|
||||
<desc id="zring-text">Ein Segment je Person, eingefärbt nach Lage.</desc>
|
||||
</svg>
|
||||
<!-- IN DER MITTE STEHT DAS SIEGEL, NICHT DAS PERSONENBILD.
|
||||
|
||||
Im Vorbild sitzt dort VanVans Firmenlogo, und das Bild von
|
||||
Dogi steckt in der Anrede daneben. Das ist kein Zufall: Der
|
||||
Ring gehoert dem BETRIEB, die Anrede der PERSON. Haette ich
|
||||
das Personenbild in die Mitte gesetzt, staende dasselbe
|
||||
Gesicht zweimal auf einer Kachel -- und der Ring saehe aus,
|
||||
als messe er die Person. -->
|
||||
<div class="zring__mitte">
|
||||
<span class="zring__siegel" aria-hidden="true"></span>
|
||||
<strong class="zring__prozent" id="zring-prozent">–</strong>
|
||||
<span class="zring__unter" id="zring-unter">wird geladen</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==== MITTE: WER, WO, WAS ================================== -->
|
||||
<div class="willkommen__text">
|
||||
<h1 class="titel" id="gruss">Willkommen</h1>
|
||||
<!-- Die Anrede sitzt in einer eigenen Fassung, wie im Vorbild:
|
||||
Bild, Name und Rolle gehoeren zusammen und heben sich
|
||||
dadurch vom Titel darunter ab, statt mit ihm zu ringen. -->
|
||||
<span class="zgruss">
|
||||
<span class="zgruss__bild willkommen__plakette" id="plakette" aria-hidden="true"></span>
|
||||
<span class="zgruss__text">
|
||||
<h1 class="titel" id="gruss">Willkommen</h1>
|
||||
<span class="zgruss__datum" id="zgruss-datum"></span>
|
||||
</span>
|
||||
<span class="zgruss__rolle" id="zgruss-rolle" hidden></span>
|
||||
</span>
|
||||
|
||||
<!-- Die Zierzeile traegt die Marke, wie "◆ VAN'S DIY ◆" im
|
||||
Vorbild. Die Rauten sind eigene Elemente und kein Zeichen im
|
||||
Text: Ein ◆ aus der Schriftart sitzt nie auf der Grundlinie
|
||||
und sieht auf jedem Geraet anders aus. -->
|
||||
<span class="zmarke">
|
||||
<i class="zraute" aria-hidden="true"></i>
|
||||
Dogfather Universe
|
||||
<i class="zraute" aria-hidden="true"></i>
|
||||
</span>
|
||||
<p class="ztitel" id="ztitel">Die Zentrale</p>
|
||||
<p class="unterzeile">
|
||||
<span id="rollentext">…</span>
|
||||
<span class="willkommen__lage" id="lage" hidden></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- ==== Die Mitte der Konsole: drei Ablesungen ================
|
||||
Zwischen Text und Uhr standen rund 300 px Leere -- auf einem
|
||||
1440er Bildschirm nachgemessen, nicht geschaetzt. Eine
|
||||
Konsole mit einem leeren Feld in der Mitte sieht nicht
|
||||
aufgeraeumt aus, sondern unfertig.
|
||||
<!-- ==== Die drei Ablesungen ==================================
|
||||
Sie standen zuerst RECHTS, zwischen Text und Uhr -- dort
|
||||
fuellten sie eine Luecke. Im Vorbild stehen sie unter dem
|
||||
Titel in der Mitte, und das ist der bessere Ort: Sie
|
||||
gehoeren zum Satz darueber ("253 Produkte, 100 Akten
|
||||
fertig, 159 Angaben offen" liest sich als Fortsetzung des
|
||||
Textes, nicht als Anzeige am Rand).
|
||||
|
||||
Was hier steht, ist deshalb keine Deko, sondern das, was der
|
||||
Lagesatz daneben NICHT sagt: wie viel heute ansteht, wann das
|
||||
Naechste losgeht, wie viel offen ist.
|
||||
KEINE ZWEITE ZAEHLUNG. Die Zahlen kommen aus denselben
|
||||
Quellen, die die Kacheln darunter fuellen. Zwei Rechenwege
|
||||
fuer dieselbe Zahl laufen auseinander, und dann stehen zwei
|
||||
Wahrheiten auf einem Bildschirm.
|
||||
|
||||
KEINE ZWEITE ZAEHLUNG. Die Zahlen kommen aus denselben zwei
|
||||
Quellen, die die Kacheln darunter fuellen (`hinweise` und die
|
||||
Tagestermine). Zwei Rechenwege fuer dieselbe Zahl laufen
|
||||
auseinander, und dann stehen zwei Wahrheiten auf einem
|
||||
Bildschirm.
|
||||
|
||||
Versteckt, solange nichts geladen ist: Drei Striche in einer
|
||||
Fassung sehen aus wie ein Fehler. -->
|
||||
Versteckt, solange nichts geladen ist: Drei Striche in
|
||||
einer Fassung sehen aus wie ein Fehler. -->
|
||||
<div class="willkommen__stand" id="tagesstand" hidden>
|
||||
<div class="stand" data-stand="heute">
|
||||
<span class="stand__wert" id="stand-heute">–</span>
|
||||
@@ -87,7 +143,8 @@
|
||||
<span class="stand__wert" id="stand-offen">–</span>
|
||||
<span class="stand__schild">Offen</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.willkommen__text -->
|
||||
|
||||
<!-- ==== Rechts in der Kachel: Glocke und Uhr ==================
|
||||
Die Glocke stand bisher in der Kopfleiste. Dort ist sie eine
|
||||
|
||||
Reference in New Issue
Block a user