feat(startseite): Giorgio entfernt, Belegschaft auf die fünf Agenten
Auf ausdrücklichen Wunsch: Giorgio entfällt vollständig. Das weicht bewusst von §6 des Auftrags ab, der ihn als Personalverwalter und Vorgesetzten vorsah — festgehalten, damit die Abweichung später nachvollziehbar bleibt. Entfernt: Rollendefinition `AGENT_SUPERVISOR`, sein gezeichnetes Porträt, sein Eintrag in der Auslastung und in der Belegschaftsansicht sowie die Erwähnung in der Produkttour. Die Personalverwaltung bleibt unverändert über den Menüpunkt «Meine Agenten» erreichbar — sie brauchte nie eine eigene Person am Bildschirm. Mit Giorgio entfällt auch die einzige Kennzahl, die aus den Freigabevorgängen kam; `computeAgentWorkload` braucht `agentWorkItems` deshalb nicht mehr. Ordner und Komponenten heissen jetzt nach ihrem Inhalt: `components/staff/`, `AgentStaffOverview`, `StaffBackdrop`. «Meetingraum» war seit dem Umbau auf Personenkarten irreführend, und ein falscher Name kostet den Nächsten mehr Zeit als die Umbenennung. Fünf statt sechs Karten heisst Flexlayout statt Dreierraster: drei oben, zwei mittig darunter, gleich breit und ohne Lücke. Die Karten geben ihre Höhe nicht mehr selbst vor — mit `height: 100%` beanspruchte jede die volle Fläche und die zweite Zeile fiel aus dem sichtbaren Bereich. Geprüft: 433 Tests, Typecheck, Build, ESLint, Token-Schwelle (889/925). Browser 1280–1920 px und Tablet: fünf Karten vollständig sichtbar, keine Überlappung, Titel und «Filter & Übersicht» beim Einstieg sichtbar, alle fünf Klicks auf der richtigen Subpage, «Meine Agenten» weiterhin erreichbar, keine Konsolenfehler, keine 404. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
|
Before Width: | Height: | Size: 477 KiB After Width: | Height: | Size: 477 KiB |
@@ -1,60 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200" role="img">
|
|
||||||
<title>Giorgio</title>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="g-bg" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#efe9e0"/>
|
|
||||||
<stop offset="1" stop-color="#ddd5c9"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="g-suit" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#2c3a4f"/>
|
|
||||||
<stop offset="1" stop-color="#1c2637"/>
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath id="g-clip">
|
|
||||||
<rect x="0" y="0" width="200" height="200"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<g clip-path="url(#g-clip)">
|
|
||||||
<rect width="200" height="200" fill="url(#g-bg)"/>
|
|
||||||
|
|
||||||
<!-- Schultern und Sakko -->
|
|
||||||
<path d="M100 128c34 0 62 20 68 48 3 13 4 20 4 24H28c0-4 1-11 4-24 6-28 34-48 68-48z" fill="url(#g-suit)"/>
|
|
||||||
<!-- Hemdkragen -->
|
|
||||||
<path d="M100 128l18 8-18 30-18-30z" fill="#f3f1ec"/>
|
|
||||||
<path d="M100 166l-9-16 9 4 9-4z" fill="#7c5f3a"/>
|
|
||||||
<path d="M100 154l7 5-3 41h-8l-3-41z" fill="#7c5f3a"/>
|
|
||||||
<!-- Reversgrat -->
|
|
||||||
<path d="M82 136l18 22-24 42-8-52z" fill="#26334a"/>
|
|
||||||
<path d="M118 136l-18 22 24 42 8-52z" fill="#26334a"/>
|
|
||||||
|
|
||||||
<!-- Hals -->
|
|
||||||
<path d="M84 104h32v28c0 4-16 10-16 10s-16-6-16-10z" fill="#c9a184"/>
|
|
||||||
<path d="M84 110c9 7 23 7 32 0v8c-9 7-23 7-32 0z" fill="#b08a70" opacity=".5"/>
|
|
||||||
|
|
||||||
<!-- Kopf -->
|
|
||||||
<ellipse cx="100" cy="80" rx="34" ry="40" fill="#dcb193"/>
|
|
||||||
<!-- Ohren -->
|
|
||||||
<ellipse cx="66" cy="82" rx="6" ry="9" fill="#d3a688"/>
|
|
||||||
<ellipse cx="134" cy="82" rx="6" ry="9" fill="#d3a688"/>
|
|
||||||
|
|
||||||
<!-- Haar, grau meliert -->
|
|
||||||
<path d="M100 34c22 0 36 14 36 33 0 5-1 10-2 13-2-3-3-9-3-14-8 4-20 6-31 6-11 0-21-2-28-6-1 5-2 11-4 14-1-3-2-8-2-13 0-19 12-33 34-33z" fill="#8d8d8f"/>
|
|
||||||
<path d="M100 34c14 0 24 6 30 16-9-6-19-9-30-9s-21 3-30 9c6-10 16-16 30-16z" fill="#a5a5a7"/>
|
|
||||||
|
|
||||||
<!-- Brille -->
|
|
||||||
<g fill="none" stroke="#3b4350" stroke-width="3">
|
|
||||||
<rect x="72" y="70" width="24" height="18" rx="6"/>
|
|
||||||
<rect x="104" y="70" width="24" height="18" rx="6"/>
|
|
||||||
<path d="M96 78h8M72 76l-8 3M128 76l8 3"/>
|
|
||||||
</g>
|
|
||||||
<!-- Augen -->
|
|
||||||
<ellipse cx="84" cy="79" rx="3" ry="3.4" fill="#3a3128"/>
|
|
||||||
<ellipse cx="116" cy="79" rx="3" ry="3.4" fill="#3a3128"/>
|
|
||||||
<!-- Brauen -->
|
|
||||||
<path d="M74 63c6-3 14-3 20 0" stroke="#8d8d8f" stroke-width="3.5" fill="none" stroke-linecap="round"/>
|
|
||||||
<path d="M106 63c6-3 14-3 20 0" stroke="#8d8d8f" stroke-width="3.5" fill="none" stroke-linecap="round"/>
|
|
||||||
<!-- Nase und Mund -->
|
|
||||||
<path d="M100 84v10c0 2-2 3-4 4" stroke="#bc8f74" stroke-width="3" fill="none" stroke-linecap="round"/>
|
|
||||||
<path d="M89 106c7 4 15 4 22 0" stroke="#a9705a" stroke-width="3.2" fill="none" stroke-linecap="round"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -66,7 +66,7 @@ export const WORKSPACE_CONFIG: Record<WorkspaceType, WorkspaceConfig> = {
|
|||||||
chipColor: '#152642',
|
chipColor: '#152642',
|
||||||
navItems: [
|
navItems: [
|
||||||
// Runde 5, §2.1/§2.2: «Übersicht» ist entfernt, «Meine Objekte» heisst im
|
// Runde 5, §2.1/§2.2: «Übersicht» ist entfernt, «Meine Objekte» heisst im
|
||||||
// Menü «Startseite». Der Meetingraum und die Objektverwaltung liegen auf
|
// Menü «Startseite». Die Belegschaft und die Objektverwaltung liegen auf
|
||||||
// derselben Seite — zwei Einträge für eine Seite wären ein toter Link.
|
// derselben Seite — zwei Einträge für eine Seite wären ein toter Link.
|
||||||
{ path: ROUTES.SUPPLY.PROPERTIES, label: HOME_NAV_LABEL, icon: Building2 },
|
{ path: ROUTES.SUPPLY.PROPERTIES, label: HOME_NAV_LABEL, icon: Building2 },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { DS_ACCENT, DS_BRAND, DS_TEXT, LEAD_CHANNEL_META } from '../../lib/ds'
|
|||||||
* Sie erklärt die Anwendung, wie sie seit dieser Runde aufgebaut ist: der
|
* Sie erklärt die Anwendung, wie sie seit dieser Runde aufgebaut ist: der
|
||||||
* entfernte Bereich «Übersicht» kommt nicht mehr vor, der Menüeintrag heisst
|
* entfernte Bereich «Übersicht» kommt nicht mehr vor, der Menüeintrag heisst
|
||||||
* «Startseite», und «Meine Objekte» bezeichnet ausschliesslich den
|
* «Startseite», und «Meine Objekte» bezeichnet ausschliesslich den
|
||||||
* Objektbereich unterhalb des Meetingraums.
|
* Objektbereich unterhalb der Belegschaft.
|
||||||
*
|
*
|
||||||
* Die Tour zeigt Bilder und Text in einem Dialog und markiert keine Elemente
|
* Die Tour zeigt Bilder und Text in einem Dialog und markiert keine Elemente
|
||||||
* auf der Seite. Das ist Absicht: eine Tour, die auf DOM-Knoten zeigt, läuft
|
* auf der Seite. Das ist Absicht: eine Tour, die auf DOM-Knoten zeigt, läuft
|
||||||
@@ -73,12 +73,12 @@ function SlideStartseite() {
|
|||||||
<SlideFrame
|
<SlideFrame
|
||||||
icon={<Building2 size={48} color={DS_BRAND.main} />}
|
icon={<Building2 size={48} color={DS_BRAND.main} />}
|
||||||
title={`Ihre ${HOME_NAV_LABEL}`}
|
title={`Ihre ${HOME_NAV_LABEL}`}
|
||||||
lead="Der Einstieg ist der Besprechungsraum Ihrer digitalen Mitarbeitenden — darunter folgt Ihr Objektbestand."
|
lead="Der Einstieg zeigt Ihre digitalen Mitarbeitenden im Dienst — darunter folgt Ihr Objektbestand."
|
||||||
>
|
>
|
||||||
<FeatureRow
|
<FeatureRow
|
||||||
icon={<MousePointerClick size={18} />}
|
icon={<MousePointerClick size={18} />}
|
||||||
color={DS_BRAND.main}
|
color={DS_BRAND.main}
|
||||||
text="Ein Klick auf einen Agenten am Tisch öffnet direkt dessen Arbeitsbereich."
|
text="Jede Karte zeigt, woran der Mitarbeitende gerade sitzt. Ein Klick öffnet seinen Arbeitsbereich."
|
||||||
/>
|
/>
|
||||||
<FeatureRow
|
<FeatureRow
|
||||||
icon={<Building2 size={18} />}
|
icon={<Building2 size={18} />}
|
||||||
@@ -119,7 +119,7 @@ function SlideAgenten() {
|
|||||||
<FeatureRow
|
<FeatureRow
|
||||||
icon={<ClipboardList size={18} />}
|
icon={<ClipboardList size={18} />}
|
||||||
color={DS_ACCENT.violet.main}
|
color={DS_ACCENT.violet.main}
|
||||||
text="Giorgio am Kopfende des Tisches führt in die Personalverwaltung: Aufgaben, Kanäle & Systeme, Bearbeitungsverlauf."
|
text="Über «Meine Agenten» verwalten Sie Ihr Personal: Aufgaben, Kanäle & Systeme und den Bearbeitungsverlauf."
|
||||||
/>
|
/>
|
||||||
</SlideFrame>
|
</SlideFrame>
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-1
@@ -58,7 +58,10 @@ export const AgentStaffCard = memo(function AgentStaffCard({ presence, onOpen }:
|
|||||||
gap: 1.25,
|
gap: 1.25,
|
||||||
p: { xs: 1.75, xl: 2 },
|
p: { xs: 1.75, xl: 2 },
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
height: '100%',
|
// Keine eigene Höhe: im Flexlayout gibt die Zeile die Höhe vor, und alle
|
||||||
|
// Karten einer Zeile werden von selbst gleich hoch. Mit `height: 100%`
|
||||||
|
// beanspruchte jede Karte die volle Fläche und die zweite Zeile fiel
|
||||||
|
// aus dem sichtbaren Bereich.
|
||||||
// Glasfläche statt Vollton: das Büro dahinter bleibt spürbar, ohne die
|
// Glasfläche statt Vollton: das Büro dahinter bleibt spürbar, ohne die
|
||||||
// Schrift zu stören.
|
// Schrift zu stören.
|
||||||
bgcolor: 'rgba(255,255,255,0.06)',
|
bgcolor: 'rgba(255,255,255,0.06)',
|
||||||
+23
-15
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { useNavigate } from 'react-router'
|
import { useNavigate } from 'react-router'
|
||||||
import { Box, Typography } from '@mui/material'
|
import { Box, Typography } from '@mui/material'
|
||||||
import { MeetingRoomScene } from './MeetingRoomScene'
|
import { StaffBackdrop } from './StaffBackdrop'
|
||||||
import { AgentStaffCard } from './AgentStaffCard'
|
import { AgentStaffCard } from './AgentStaffCard'
|
||||||
import { useAgentPresence } from '../../hooks/useAgentPresence'
|
import { useAgentPresence } from '../../hooks/useAgentPresence'
|
||||||
import { WorkloadTone } from '../../domain/agentWorkload'
|
import { WorkloadTone } from '../../domain/agentWorkload'
|
||||||
@@ -24,12 +24,13 @@ const RESERVED_PX = TOP_BAR_PX + KEEP_VISIBLE_BELOW_PX
|
|||||||
const MIN_SCENE_PX = 420
|
const MIN_SCENE_PX = 420
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Die digitale Belegschaft auf der Startseite (Runde 5, §3/§6).
|
* Die digitale Belegschaft auf der Startseite (Runde 5, §3).
|
||||||
*
|
*
|
||||||
* Sechs Mitarbeitende an ihrem Arbeitsplatz — mit Porträt, Funktion,
|
* Die fünf Mitarbeitenden an ihrem Arbeitsplatz — mit Porträt, Funktion,
|
||||||
* Abteilung, Personalnummer, Dienstzustand, einem Satz in der ersten Person
|
* Abteilung, Personalnummer, Dienstzustand, einem Satz in der ersten Person
|
||||||
* und der zuletzt protokollierten Handlung. Ein Klick auf eine Karte öffnet
|
* und der zuletzt protokollierten Handlung. Ein Klick auf eine Karte öffnet
|
||||||
* den jeweiligen Arbeitsbereich, bei Giorgio die Personalverwaltung.
|
* den jeweiligen Arbeitsbereich. Die Personalverwaltung selbst liegt im
|
||||||
|
* Menüpunkt «Meine Agenten» und braucht keine eigene Person am Bildschirm.
|
||||||
*
|
*
|
||||||
* Bewusst keine nachgebaute Raumperspektive mehr: eine gezeichnete Kulisse
|
* Bewusst keine nachgebaute Raumperspektive mehr: eine gezeichnete Kulisse
|
||||||
* bleibt neben fotografierten Porträts erkennbar gezeichnet, und ein Raum
|
* bleibt neben fotografierten Porträts erkennbar gezeichnet, und ein Raum
|
||||||
@@ -37,7 +38,7 @@ const MIN_SCENE_PX = 420
|
|||||||
* echten Büro — dass jemand da ist, etwas zu sagen hat und nachweisbar
|
* echten Büro — dass jemand da ist, etwas zu sagen hat und nachweisbar
|
||||||
* gearbeitet hat.
|
* gearbeitet hat.
|
||||||
*/
|
*/
|
||||||
export function AgentMeetingRoom() {
|
export function AgentStaffOverview() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { data: staff } = useAgentPresence()
|
const { data: staff } = useAgentPresence()
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ export function AgentMeetingRoom() {
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MeetingRoomScene />
|
<StaffBackdrop />
|
||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -88,14 +89,21 @@ export function AgentMeetingRoom() {
|
|||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
display: 'grid',
|
// Flexlayout statt Raster: bei fünf Karten bliebe in einem
|
||||||
gridTemplateColumns: {
|
// Dreierraster unten eine Lücke. So stehen drei oben und zwei
|
||||||
xs: '1fr',
|
// mittig darunter — gleich breit, ohne Loch.
|
||||||
sm: 'repeat(2, minmax(0, 1fr))',
|
display: 'flex',
|
||||||
lg: 'repeat(3, minmax(0, 1fr))',
|
flexWrap: 'wrap',
|
||||||
},
|
justifyContent: 'center',
|
||||||
gridAutoRows: '1fr',
|
alignContent: 'center',
|
||||||
gap: { xs: 1.5, lg: 2 },
|
gap: { xs: 1.5, lg: 2 },
|
||||||
|
'& > *': {
|
||||||
|
flex: {
|
||||||
|
xs: '1 1 100%',
|
||||||
|
sm: '0 1 calc((100% - 12px) / 2)',
|
||||||
|
lg: '0 1 calc((100% - 32px) / 3)',
|
||||||
|
},
|
||||||
|
},
|
||||||
// Auf niedrigen Fenstern darf die Belegschaft scrollen, statt die
|
// Auf niedrigen Fenstern darf die Belegschaft scrollen, statt die
|
||||||
// Karten bis zur Unlesbarkeit zu stauchen.
|
// Karten bis zur Unlesbarkeit zu stauchen.
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
@@ -114,8 +122,8 @@ export function AgentMeetingRoom() {
|
|||||||
* Anriss über der Belegschaft.
|
* Anriss über der Belegschaft.
|
||||||
*
|
*
|
||||||
* Beantwortet die Frage, mit der jemand diese Seite öffnet: muss ich heute
|
* Beantwortet die Frage, mit der jemand diese Seite öffnet: muss ich heute
|
||||||
* irgendwo eingreifen? Ohne den Satz müsste man sechs Karten lesen und selbst
|
* irgendwo eingreifen? Ohne den Satz müsste man jede Karte einzeln lesen und
|
||||||
* zusammenzählen.
|
* selbst zusammenzählen.
|
||||||
*/
|
*/
|
||||||
function StaffHeadline({ urgent, onDuty, total }: { urgent: number; onDuty: number; total: number }) {
|
function StaffHeadline({ urgent, onDuty, total }: { urgent: number; onDuty: number; total: number }) {
|
||||||
const summary =
|
const summary =
|
||||||
+2
-2
@@ -4,7 +4,7 @@ import { ROOM_BACKDROP } from './roomBackdrop'
|
|||||||
import { DS_NEUTRAL } from '../../lib/ds'
|
import { DS_NEUTRAL } from '../../lib/ds'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bildhintergrund des Agenten-Meetingraums (Runde 5, §3).
|
* Bildhintergrund der Belegschaftsansicht (Runde 5, §3).
|
||||||
*
|
*
|
||||||
* Bewusst eine einzige Bildfläche mit Verlauf — und nicht länger ein aus
|
* Bewusst eine einzige Bildfläche mit Verlauf — und nicht länger ein aus
|
||||||
* Flächen konstruierter Raum. Ein selbstgezeichneter Raum bleibt neben
|
* Flächen konstruierter Raum. Ein selbstgezeichneter Raum bleibt neben
|
||||||
@@ -14,7 +14,7 @@ import { DS_NEUTRAL } from '../../lib/ds'
|
|||||||
* Die Komponente selbst weiss nichts über das Motiv. Sie zeigt, was
|
* Die Komponente selbst weiss nichts über das Motiv. Sie zeigt, was
|
||||||
* `ROOM_BACKDROP` liefert — die Aussicht heute, eine Raumaufnahme später.
|
* `ROOM_BACKDROP` liefert — die Aussicht heute, eine Raumaufnahme später.
|
||||||
*/
|
*/
|
||||||
export const MeetingRoomScene = memo(function MeetingRoomScene() {
|
export const StaffBackdrop = memo(function StaffBackdrop() {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: 'absolute', inset: 0, overflow: 'hidden', bgcolor: DS_NEUTRAL.sidebar }}>
|
<Box sx={{ position: 'absolute', inset: 0, overflow: 'hidden', bgcolor: DS_NEUTRAL.sidebar }}>
|
||||||
<Box
|
<Box
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Digitale Belegschaft auf der Startseite (Runde 5, §3). Barrel-Export (CLAUDE.md §13).
|
// Digitale Belegschaft auf der Startseite (Runde 5, §3). Barrel-Export (CLAUDE.md §13).
|
||||||
|
|
||||||
export { AgentMeetingRoom } from './AgentMeetingRoom'
|
export { AgentStaffOverview } from './AgentStaffOverview'
|
||||||
export { AgentStaffCard } from './AgentStaffCard'
|
export { AgentStaffCard } from './AgentStaffCard'
|
||||||
export { ROOM_BACKDROP } from './roomBackdrop'
|
export { ROOM_BACKDROP } from './roomBackdrop'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import buero from '../../assets/meeting-room/buero.jpg'
|
import buero from '../../assets/staff/buero.jpg'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Umgebung der Belegschaftsansicht auf der Startseite (Runde 5, §3).
|
* Umgebung der Belegschaftsansicht auf der Startseite (Runde 5, §3).
|
||||||
@@ -50,10 +50,6 @@ import gian from '../../assets/team/gian.jpg'
|
|||||||
import ida from '../../assets/team/ida.jpg'
|
import ida from '../../assets/team/ida.jpg'
|
||||||
import jana from '../../assets/team/jana.jpg'
|
import jana from '../../assets/team/jana.jpg'
|
||||||
import zeno from '../../assets/team/zeno.jpg'
|
import zeno from '../../assets/team/zeno.jpg'
|
||||||
// Giorgio ist erst in Runde 5 dazugekommen und hat kein Porträt aus dem
|
|
||||||
// Konzeptdokument. Statt den Kopf eines bestehenden Agenten zweitzuverwenden —
|
|
||||||
// jeder Kopf gehört genau einer Person — trägt er ein gezeichnetes Porträt.
|
|
||||||
import giorgio from '../../assets/team/giorgio.svg'
|
|
||||||
|
|
||||||
export const AGENT_PHOTOS: Record<string, string | undefined> = {
|
export const AGENT_PHOTOS: Record<string, string | undefined> = {
|
||||||
bruno,
|
bruno,
|
||||||
@@ -90,5 +86,4 @@ export const AGENT_PHOTOS: Record<string, string | undefined> = {
|
|||||||
ida,
|
ida,
|
||||||
jana,
|
jana,
|
||||||
zeno,
|
zeno,
|
||||||
giorgio,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Property On — aktuelle Auslastung eines digitalen Mitarbeitenden.
|
* Property On — aktuelle Auslastung eines digitalen Mitarbeitenden.
|
||||||
*
|
*
|
||||||
* Der Meetingraum auf der Startseite zeigt nicht nur, *wer* für einen
|
* Die Belegschaftsansicht der Startseite zeigt nicht nur, *wer* für einen
|
||||||
* arbeitet, sondern *woran gerade*. Ohne diese Zahl wäre die Szene Dekoration;
|
* arbeitet, sondern *woran gerade*. Ohne diese Zahl wäre die Szene Dekoration;
|
||||||
* mit ihr ist sie eine Lageübersicht — genau der Anspruch aus CLAUDE.md §1:
|
* mit ihr ist sie eine Lageübersicht — genau der Anspruch aus CLAUDE.md §1:
|
||||||
* jeder Bildschirm ist um eine Entscheidung herum gebaut.
|
* jeder Bildschirm ist um eine Entscheidung herum gebaut.
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import { useVisitAssignments } from './useVisitAssignments'
|
|||||||
import { useExposeLeads } from './useExposeLeads'
|
import { useExposeLeads } from './useExposeLeads'
|
||||||
import { useUnifiedLeads } from './useUnifiedLeads'
|
import { useUnifiedLeads } from './useUnifiedLeads'
|
||||||
import { useProperties } from './useProperties'
|
import { useProperties } from './useProperties'
|
||||||
import { useAgentWorkItems } from './useAgentWorkItems'
|
|
||||||
import { computeAgentWorkload } from '../services/agentWorkloadService'
|
import { computeAgentWorkload } from '../services/agentWorkloadService'
|
||||||
import type { AgentWorkload } from '../domain/agentWorkload'
|
import type { AgentWorkload } from '../domain/agentWorkload'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aktuelle Auslastung aller sechs digitalen Mitarbeitenden (Runde 5).
|
* Aktuelle Auslastung der fünf digitalen Mitarbeitenden (Runde 5).
|
||||||
*
|
*
|
||||||
* Der Hook sammelt nur die Bestände ein; was daraus wird, entscheidet
|
* Der Hook sammelt nur die Bestände ein; was daraus wird, entscheidet
|
||||||
* `agentWorkloadService` — Schwellen, Zeitfenster und Formulierungen sind
|
* `agentWorkloadService` — Schwellen, Zeitfenster und Formulierungen sind
|
||||||
@@ -22,17 +21,16 @@ export function useAgentWorkload(): { data: Record<string, AgentWorkload>; isLoa
|
|||||||
const { data: exposeLeads = [], isLoading: exposeLoading } = useExposeLeads()
|
const { data: exposeLeads = [], isLoading: exposeLoading } = useExposeLeads()
|
||||||
const { data: leads, isLoading: leadsLoading } = useUnifiedLeads()
|
const { data: leads, isLoading: leadsLoading } = useUnifiedLeads()
|
||||||
const { data: properties = [], isLoading: propertiesLoading } = useProperties()
|
const { data: properties = [], isLoading: propertiesLoading } = useProperties()
|
||||||
const { data: workItems = [], isLoading: workItemsLoading } = useAgentWorkItems()
|
|
||||||
|
|
||||||
const data = useMemo(
|
const data = useMemo(
|
||||||
() => computeAgentWorkload({ reminders, visits, exposeLeads, leads, properties, workItems }),
|
() => computeAgentWorkload({ reminders, visits, exposeLeads, leads, properties }),
|
||||||
[reminders, visits, exposeLeads, leads, properties, workItems],
|
[reminders, visits, exposeLeads, leads, properties],
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
isLoading:
|
isLoading:
|
||||||
remindersLoading || visitsLoading || exposeLoading ||
|
remindersLoading || visitsLoading || exposeLoading ||
|
||||||
leadsLoading || propertiesLoading || workItemsLoading,
|
leadsLoading || propertiesLoading,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,6 @@
|
|||||||
import { ROUTES } from './constants'
|
import { ROUTES } from './constants'
|
||||||
|
|
||||||
export interface AgentWorkspace {
|
export interface AgentWorkspace {
|
||||||
/** Abteilung laut Personalblatt. */
|
|
||||||
department?: string
|
|
||||||
/** Personalnummer laut Personalblatt. */
|
|
||||||
personnelNumber?: string
|
|
||||||
/** Identisch mit der Agenten-ID in `mock-data/agents/` und `AGENT_PHOTOS`. */
|
/** Identisch mit der Agenten-ID in `mock-data/agents/` und `AGENT_PHOTOS`. */
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -37,22 +33,3 @@ export const AGENT_WORKSPACES: AgentWorkspace[] = [
|
|||||||
export function agentWorkspaceById(id: string): AgentWorkspace | undefined {
|
export function agentWorkspaceById(id: string): AgentWorkspace | undefined {
|
||||||
return AGENT_WORKSPACES.find(a => a.id === id)
|
return AGENT_WORKSPACES.find(a => a.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Giorgio — Personalverwalter und Vorgesetzter der fünf Agenten (Runde 5, §6).
|
|
||||||
*
|
|
||||||
* Bewusst nicht Teil von `AGENT_WORKSPACES`: Giorgio hat keine eigene
|
|
||||||
* Arbeitsseite, sondern steht für die Adminfläche selbst. Sein Ziel ist
|
|
||||||
* «Meine Agenten» mit den Bereichen Personalverwaltung, Kanäle & Systeme und
|
|
||||||
* Bearbeitungsverlauf. Stünde er in derselben Liste, erschiene er als sechster
|
|
||||||
* Eintrag im Seitenmenü und als sechster Mitarbeitender in der Belegschaft —
|
|
||||||
* beides wäre falsch.
|
|
||||||
*/
|
|
||||||
export const AGENT_SUPERVISOR: AgentWorkspace = {
|
|
||||||
id: 'giorgio',
|
|
||||||
name: 'Giorgio',
|
|
||||||
role: 'Personalverwalter',
|
|
||||||
department: 'Personalwesen',
|
|
||||||
personnelNumber: 'PO-ZD-01',
|
|
||||||
path: ROUTES.SUPPLY.TEAM,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Box, Collapse, Drawer, Typography, useMediaQuery, useTheme } from '@mui
|
|||||||
import { ChevronDown, ChevronUp } from 'lucide-react'
|
import { ChevronDown, ChevronUp } from 'lucide-react'
|
||||||
import { PageHeader } from '../../components/layout'
|
import { PageHeader } from '../../components/layout'
|
||||||
import { ViewToggle } from '../../components/shared'
|
import { ViewToggle } from '../../components/shared'
|
||||||
import { AgentMeetingRoom } from '../../components/meeting-room'
|
import { AgentStaffOverview } from '../../components/staff'
|
||||||
import { useProperties } from '../../hooks/useProperties'
|
import { useProperties } from '../../hooks/useProperties'
|
||||||
import { useActiveInquiries } from '../../hooks/useInquiries'
|
import { useActiveInquiries } from '../../hooks/useInquiries'
|
||||||
import { PropertyFilterBar, PropertyTable, PropertyDetailView, PropertyIntelligenceCard } from '../../components/supply'
|
import { PropertyFilterBar, PropertyTable, PropertyDetailView, PropertyIntelligenceCard } from '../../components/supply'
|
||||||
@@ -95,13 +95,13 @@ export default function Properties() {
|
|||||||
const filtered = useMemo(() => applyFilters(properties, filters), [properties, filters])
|
const filtered = useMemo(() => applyFilters(properties, filters), [properties, filters])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Runde 5, §2.3: Die Startseite scrollt als Ganzes. Der Meetingraum steht
|
// Runde 5, §2.3: Die Startseite scrollt als Ganzes. Die Belegschaft steht
|
||||||
// oben und misst sich an der Viewport-Höhe; Titel und Filterbalken bleiben
|
// oben und misst sich an der Viewport-Höhe; Titel und Filterbalken bleiben
|
||||||
// beim Einstieg gerade noch sichtbar, die Objektliste beginnt darunter.
|
// beim Einstieg gerade noch sichtbar, die Objektliste beginnt darunter.
|
||||||
// Ein eigener Scrollbereich für die Tabelle wäre ein zweiter Scrollbalken
|
// Ein eigener Scrollbereich für die Tabelle wäre ein zweiter Scrollbalken
|
||||||
// im selben Bild und würde genau das verhindern.
|
// im selben Bild und würde genau das verhindern.
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100%' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100%' }}>
|
||||||
<AgentMeetingRoom />
|
<AgentStaffOverview />
|
||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={MY_PROPERTIES_LABEL}
|
title={MY_PROPERTIES_LABEL}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function workload(agentId: string): AgentWorkload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WORKLOAD = Object.fromEntries(
|
const WORKLOAD = Object.fromEntries(
|
||||||
['ferdi', 'bruno', 'livia', 'nora', 'sina', 'giorgio'].map(id => [id, workload(id)]),
|
['ferdi', 'bruno', 'livia', 'nora', 'sina'].map(id => [id, workload(id)]),
|
||||||
)
|
)
|
||||||
|
|
||||||
function input(partial: Partial<AgentPresenceInput> = {}): AgentPresenceInput {
|
function input(partial: Partial<AgentPresenceInput> = {}): AgentPresenceInput {
|
||||||
@@ -72,9 +72,9 @@ describe('Zeitangabe in Alltagssprache', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Belegschaft der Startseite', () => {
|
describe('Belegschaft der Startseite', () => {
|
||||||
it('führt alle sechs in verbindlicher Reihenfolge, Giorgio zuletzt', () => {
|
it('führt die fünf Agenten in verbindlicher Reihenfolge', () => {
|
||||||
const staff = buildAgentPresence(input(), NOW)
|
const staff = buildAgentPresence(input(), NOW)
|
||||||
expect(staff.map(p => p.agentId)).toEqual(['ferdi', 'bruno', 'livia', 'nora', 'sina', 'giorgio'])
|
expect(staff.map(p => p.agentId)).toEqual(['ferdi', 'bruno', 'livia', 'nora', 'sina'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('übernimmt Personalien aus dem Personalblatt', () => {
|
it('übernimmt Personalien aus dem Personalblatt', () => {
|
||||||
@@ -88,13 +88,6 @@ describe('Belegschaft der Startseite', () => {
|
|||||||
expect(ferdi.autonomy).toBe('Autonom')
|
expect(ferdi.autonomy).toBe('Autonom')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('gibt Giorgio Personalien und Stufe aus der Rollendefinition — er hat kein Dossier', () => {
|
|
||||||
const giorgio = buildAgentPresence(input(), NOW).find(p => p.agentId === 'giorgio')!
|
|
||||||
expect(giorgio.department).toBe('Personalwesen')
|
|
||||||
expect(giorgio.personnelNumber).toBe('PO-ZD-01')
|
|
||||||
expect(giorgio.autonomy).toBe('Vorgesetzter')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('zeigt einen pausierten Mitarbeitenden als nicht im Dienst', () => {
|
it('zeigt einen pausierten Mitarbeitenden als nicht im Dienst', () => {
|
||||||
const staff = buildAgentPresence(
|
const staff = buildAgentPresence(
|
||||||
input({ agents: [dossier('sina', { status: AgentStatus.PAUSED })] }),
|
input({ agents: [dossier('sina', { status: AgentStatus.PAUSED })] }),
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import type { AgentWorkloadInput } from '../agentWorkloadService'
|
|||||||
import { WorkloadTone } from '../../domain/agentWorkload'
|
import { WorkloadTone } from '../../domain/agentWorkload'
|
||||||
import { ReminderStatus } from '../../domain/reminder'
|
import { ReminderStatus } from '../../domain/reminder'
|
||||||
import { ExposeLeadStatus } from '../../domain/exposeLead'
|
import { ExposeLeadStatus } from '../../domain/exposeLead'
|
||||||
import { AgentWorkItemStatus } from '../../domain/agentWorkItem'
|
|
||||||
|
|
||||||
const NOW = Date.parse('2026-08-06T12:00:00.000Z')
|
const NOW = Date.parse('2026-08-06T12:00:00.000Z')
|
||||||
const DAY = 24 * 60 * 60 * 1000
|
const DAY = 24 * 60 * 60 * 1000
|
||||||
@@ -24,7 +23,7 @@ function iso(offsetDays: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY: AgentWorkloadInput = {
|
const EMPTY: AgentWorkloadInput = {
|
||||||
reminders: [], visits: [], exposeLeads: [], leads: [], properties: [], workItems: [],
|
reminders: [], visits: [], exposeLeads: [], leads: [], properties: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
function input(partial: Partial<AgentWorkloadInput>): AgentWorkloadInput {
|
function input(partial: Partial<AgentWorkloadInput>): AgentWorkloadInput {
|
||||||
@@ -92,22 +91,6 @@ describe('Auslastung je Agent', () => {
|
|||||||
expect(w.label).toBe('2 Objekte mit Datenlücken')
|
expect(w.label).toBe('2 Objekte mit Datenlücken')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('zählt bei Giorgio die offenen Freigaben', () => {
|
|
||||||
const w = computeAgentWorkload(
|
|
||||||
input({
|
|
||||||
workItems: [
|
|
||||||
{ status: AgentWorkItemStatus.PENDING },
|
|
||||||
{ status: AgentWorkItemStatus.PENDING },
|
|
||||||
{ status: AgentWorkItemStatus.APPROVED },
|
|
||||||
] as AgentWorkloadInput['workItems'],
|
|
||||||
}),
|
|
||||||
NOW,
|
|
||||||
).giorgio
|
|
||||||
|
|
||||||
expect(w.count).toBe(2)
|
|
||||||
expect(w.label).toBe('2 Freigaben offen')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('hält Noras Zahl nie für dringend — sie schlägt vor, sie entscheidet nicht', () => {
|
it('hält Noras Zahl nie für dringend — sie schlägt vor, sie entscheidet nicht', () => {
|
||||||
const leads = Array.from({ length: 40 }, () => ({ receivedAt: iso(-1) }))
|
const leads = Array.from({ length: 40 }, () => ({ receivedAt: iso(-1) }))
|
||||||
const w = computeAgentWorkload(
|
const w = computeAgentWorkload(
|
||||||
@@ -131,7 +114,7 @@ describe('Auslastung je Agent', () => {
|
|||||||
it('meldet bei leerer Datenlage überall Ruhe statt Lücken', () => {
|
it('meldet bei leerer Datenlage überall Ruhe statt Lücken', () => {
|
||||||
const all = computeAgentWorkload(EMPTY, NOW)
|
const all = computeAgentWorkload(EMPTY, NOW)
|
||||||
|
|
||||||
expect(Object.keys(all)).toEqual(['ferdi', 'bruno', 'livia', 'nora', 'sina', 'giorgio'])
|
expect(Object.keys(all)).toEqual(['ferdi', 'bruno', 'livia', 'nora', 'sina'])
|
||||||
for (const w of Object.values(all)) {
|
for (const w of Object.values(all)) {
|
||||||
expect(w.count).toBe(0)
|
expect(w.count).toBe(0)
|
||||||
expect(w.tone).toBe(WorkloadTone.CALM)
|
expect(w.tone).toBe(WorkloadTone.CALM)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { AgentWorkload } from '../domain/agentWorkload'
|
|||||||
import type { AgentProtocolEntry } from '../domain/agentProtocol'
|
import type { AgentProtocolEntry } from '../domain/agentProtocol'
|
||||||
import type { TeamAgent } from '../domain/teamAgent'
|
import type { TeamAgent } from '../domain/teamAgent'
|
||||||
import { AgentStatus } from '../domain/teamAgent'
|
import { AgentStatus } from '../domain/teamAgent'
|
||||||
import { AGENT_SUPERVISOR, AGENT_WORKSPACES } from '../lib/agentWorkspaces'
|
import { AGENT_WORKSPACES } from '../lib/agentWorkspaces'
|
||||||
import { AGENT_AUTONOMY_LABELS } from '../lib/constants'
|
import { AGENT_AUTONOMY_LABELS } from '../lib/constants'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,10 +13,6 @@ import { AGENT_AUTONOMY_LABELS } from '../lib/constants'
|
|||||||
* als «letzte Handlung» gilt und wie eine Zeitangabe in Alltagssprache lautet,
|
* als «letzte Handlung» gilt und wie eine Zeitangabe in Alltagssprache lautet,
|
||||||
* sind fachliche Festlegungen. Der Stichzeitpunkt ist ein Parameter — dieselbe
|
* sind fachliche Festlegungen. Der Stichzeitpunkt ist ein Parameter — dieselbe
|
||||||
* Datenlage ergibt damit immer dasselbe Ergebnis und lässt sich prüfen.
|
* Datenlage ergibt damit immer dasselbe Ergebnis und lässt sich prüfen.
|
||||||
*
|
|
||||||
* Giorgio steht bewusst am Ende: er ist Vorgesetzter, kein sechster
|
|
||||||
* Sachbearbeiter, und sein Klick führt nicht auf eine Arbeitsseite, sondern in
|
|
||||||
* die Personalverwaltung.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const MINUTE = 60 * 1000
|
const MINUTE = 60 * 1000
|
||||||
@@ -73,7 +69,7 @@ export function buildAgentPresence(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const byId = new Map(agents.map(a => [a.id, a]))
|
const byId = new Map(agents.map(a => [a.id, a]))
|
||||||
const roster = [...AGENT_WORKSPACES, AGENT_SUPERVISOR]
|
const roster = AGENT_WORKSPACES
|
||||||
|
|
||||||
return roster.map(member => {
|
return roster.map(member => {
|
||||||
const dossier = byId.get(member.id)
|
const dossier = byId.get(member.id)
|
||||||
@@ -83,17 +79,13 @@ export function buildAgentPresence(
|
|||||||
agentId: member.id,
|
agentId: member.id,
|
||||||
name: member.name,
|
name: member.name,
|
||||||
role: member.role,
|
role: member.role,
|
||||||
// Personalblatt zuerst; für Giorgio, der keines hat, die Angaben aus
|
department: dossier?.department ?? '',
|
||||||
// der zentralen Rollendefinition.
|
personnelNumber: dossier?.personnelNumber ?? '',
|
||||||
department: dossier?.department ?? member.department ?? '',
|
|
||||||
personnelNumber: dossier?.personnelNumber ?? member.personnelNumber ?? '',
|
|
||||||
path: member.path,
|
path: member.path,
|
||||||
// Ohne Personalblatt gilt jemand als im Dienst: die Belegschaftsansicht
|
// Ohne Personalblatt gilt jemand als im Dienst: die Belegschaftsansicht
|
||||||
// soll niemanden fälschlich als pausiert zeigen, nur weil ein Dossier fehlt.
|
// soll niemanden fälschlich als pausiert zeigen, nur weil ein Dossier fehlt.
|
||||||
onDuty: dossier ? dossier.status === AgentStatus.ACTIVE : true,
|
onDuty: dossier ? dossier.status === AgentStatus.ACTIVE : true,
|
||||||
// Giorgio führt die Belegschaft und entscheidet nichts allein über
|
autonomy: dossier ? (AGENT_AUTONOMY_LABELS[dossier.autonomyLevel] ?? '') : '',
|
||||||
// Objekte — seine Stufe ergibt sich aus der Rolle, nicht aus einem Dossier.
|
|
||||||
autonomy: dossier ? (AGENT_AUTONOMY_LABELS[dossier.autonomyLevel] ?? '') : 'Vorgesetzter',
|
|
||||||
workload: workload[member.id],
|
workload: workload[member.id],
|
||||||
lastAction: action
|
lastAction: action
|
||||||
? { title: action.title, when: formatRelativeTime(action.timestamp, now) }
|
? { title: action.title, when: formatRelativeTime(action.timestamp, now) }
|
||||||
|
|||||||
@@ -2,18 +2,15 @@ import { ReminderStatus } from '../domain/reminder'
|
|||||||
import type { Reminder } from '../domain/reminder'
|
import type { Reminder } from '../domain/reminder'
|
||||||
import { ExposeLeadStatus } from '../domain/exposeLead'
|
import { ExposeLeadStatus } from '../domain/exposeLead'
|
||||||
import type { ExposeLead } from '../domain/exposeLead'
|
import type { ExposeLead } from '../domain/exposeLead'
|
||||||
import { AgentWorkItemStatus } from '../domain/agentWorkItem'
|
|
||||||
import type { AgentWorkItem } from '../domain/agentWorkItem'
|
|
||||||
import type { VisitAssignment } from '../domain/visitAssignment'
|
import type { VisitAssignment } from '../domain/visitAssignment'
|
||||||
import type { UnifiedLead } from '../domain/unifiedLead'
|
import type { UnifiedLead } from '../domain/unifiedLead'
|
||||||
import type { Property } from '../domain/property'
|
import type { Property } from '../domain/property'
|
||||||
import { WorkloadTone } from '../domain/agentWorkload'
|
import { WorkloadTone } from '../domain/agentWorkload'
|
||||||
import type { AgentWorkload } from '../domain/agentWorkload'
|
import type { AgentWorkload } from '../domain/agentWorkload'
|
||||||
import { AGENT_SUPERVISOR } from '../lib/agentWorkspaces'
|
|
||||||
import { DQ_MEDIUM } from '../lib/constants'
|
import { DQ_MEDIUM } from '../lib/constants'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auslastung der sechs digitalen Mitarbeitenden (Runde 5).
|
* Auslastung der fünf digitalen Mitarbeitenden (Runde 5).
|
||||||
*
|
*
|
||||||
* Reine Geschäftslogik, bewusst ausserhalb von React: was «diese Woche»
|
* Reine Geschäftslogik, bewusst ausserhalb von React: was «diese Woche»
|
||||||
* bedeutet, ab wann eine Zahl dringend ist und welcher Bestand zu welchem
|
* bedeutet, ab wann eine Zahl dringend ist und welcher Bestand zu welchem
|
||||||
@@ -38,7 +35,6 @@ const URGENT_FROM = {
|
|||||||
visits: 4,
|
visits: 4,
|
||||||
expose: 5,
|
expose: 5,
|
||||||
dataGaps: 10,
|
dataGaps: 10,
|
||||||
approvals: 5,
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** Zeitfenster der Auswertung. */
|
/** Zeitfenster der Auswertung. */
|
||||||
@@ -55,7 +51,6 @@ export interface AgentWorkloadInput {
|
|||||||
exposeLeads: ExposeLead[]
|
exposeLeads: ExposeLead[]
|
||||||
leads: UnifiedLead[]
|
leads: UnifiedLead[]
|
||||||
properties: Property[]
|
properties: Property[]
|
||||||
workItems: AgentWorkItem[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function within(iso: string, from: number, to: number): boolean {
|
function within(iso: string, from: number, to: number): boolean {
|
||||||
@@ -156,19 +151,7 @@ export function computeAgentWorkload(
|
|||||||
: `Bei ${gaps} ${plural(gaps, 'Objekt fehlen', 'Objekten fehlen')} mir Angaben. Ich melde, was ich nicht selbst finde.`,
|
: `Bei ${gaps} ${plural(gaps, 'Objekt fehlen', 'Objekten fehlen')} mir Angaben. Ich melde, was ich nicht selbst finde.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Giorgio: Freigaben in der Personalverwaltung ────────────────────────────
|
|
||||||
const pending = input.workItems.filter(w => w.status === AgentWorkItemStatus.PENDING).length
|
|
||||||
const giorgio: AgentWorkload = {
|
|
||||||
agentId: AGENT_SUPERVISOR.id,
|
|
||||||
count: pending,
|
|
||||||
label: `${pending} ${plural(pending, 'Freigabe', 'Freigaben')} offen`,
|
|
||||||
tone: tone(pending, URGENT_FROM.approvals),
|
|
||||||
voice: pending === 0
|
|
||||||
? 'Nichts liegt zur Freigabe bereit. Die Belegschaft läuft.'
|
|
||||||
: `${pending} ${plural(pending, 'Vorgang wartet', 'Vorgänge warten')} auf deine Freigabe.`,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
[ferdi, bruno, livia, nora, sina, giorgio].map(w => [w.agentId, w]),
|
[ferdi, bruno, livia, nora, sina].map(w => [w.agentId, w]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user