refactor(team): zweite Überarbeitungsrunde — Oberfläche bereinigen
Gezielte UI-Bereinigung ohne neue Architektur. Bestehende Komponenten angepasst, Datenstrukturen und Routing unverändert. GLOBAL Chip «Verwaltung» und Seitenname in der Kopfzeile entfallen auf /supply/team/*; die Seiten tragen ihren Titel bereits gross im Inhalt. Andere Module behalten die Zeile. Demo-Modus-Hinweis und alle Untertitel entfernt. Autonomiegrad aus der Oberfläche genommen — die Daten bleiben unangetastet. PERSONALVERWALTUNG Beschrieb auf zwei Sätze gekürzt, Input/Kernablauf/Output entfallen: das war Prozessnotation, sie beschrieb wie gearbeitet wird, nicht wofür man jemanden hat. Zuständigkeiten stehen jetzt im Kopfbereich statt tief im Dossier. Reiterfolge neu: Agentenbeschrieb, Aufgaben, Kennzahlen, Kanäle & Systeme, Protokoll. Kanäle und Systeme sind über eine dünne Hülle zusammengeführt — die bestehende Logik samt Konfigurationsdialog bleibt unberührt. Aufgaben dreispaltig ohne Zeitplan, Ereignisfilter im Protokoll entfernt. Der abgelöste Reiter «Systeme» wird weiterhin aufgelöst, damit bestehende Verweise nicht still auf den ersten Reiter zurückfallen. ORGANIGRAMM — die eigentliche Korrektur Der falsch gewählte Agent lag an der Geometrie: `rotate(a) translateY(r)` schiebt bei a = 0 nach UNTEN, die Drehung wurde aber als `180 − index·step` berechnet. Damit stand der gewählte Mitarbeitende oben und das Dossier darunter gehörte zum falschen. Jetzt `−index·step`. Dazu beruhigt: Zugempfindlichkeit von 0.5 auf 0.22 Grad je Pixel, 10 px Totzone gegen Zittern, kurzer Zug wechselt höchstens einen Platz, Einrasten in 250 ms, Auswahl erst nach dem Einrasten. Kreis von 720 auf 460 px. BEARBEITUNGSVERLAUF Filter ohne Vorgangstyp, Priorität und Kanal; Suche in Zeile eins, Rest in Zeile zwei. Karten tragen nur noch Agent, Titel mit Liegenschaft, zwei Zeilen Text und Zeitstempel — Priorität, Status, Vorgangstyp und Kanal standen auf praktisch jeder Karte und trugen damit keine Information mehr. Drawer ohne Verarbeitungsschritte und Aktionshistorie; nur die auslösende Nachricht plus Quicklink ins Anfragencenter statt des ganzen Verlaufs. «Fundstellen» heisst «Quellen». Der Entscheidungshinweis heisst jetzt «Hierzu brauche ich Ihre Entscheidung». Aktionsleiste: pendent Freigeben, Zurückweisen, Quellen anzeigen, Inserat anzeigen — erledigt nur «Objekt anzeigen». KANÄLE & SYSTEME, TEAMÜBERSICHT Kanäle vor Systemen als eigene Abschnitte, vier Karten pro Zeile, Beschreibung und Statusbadge raus, Regler mit Klartext Aktiv/Inaktiv/Geplant. Die Kreisgrafik skaliert nach Anzahl sichtbarer Ringe und ist auf die Viewport-Höhe begrenzt, damit das Kernteam ohne Scrollen vollständig sichtbar bleibt. Drei Testzusicherungen prüfen jetzt die Abwesenheit statt der Anwesenheit von Objekt-ID und Rückfragegrund auf der Karte — beides wurde bewusst entfernt. Typecheck grün, ESLint über die geänderten Dateien grün, Build grün, 413 Tests grün. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,30 +1,29 @@
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { Box, Divider, Drawer, IconButton, Typography } from '@mui/material'
|
||||
import { HelpCircle, X } from 'lucide-react'
|
||||
import { Box, Button, Drawer, IconButton, Typography } from '@mui/material'
|
||||
import { ArrowUpRight, HelpCircle, X } from 'lucide-react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { useTeamStore } from '../../stores/teamStore'
|
||||
import { useAgentWorkItem } from '../../hooks/useAgentWorkItems'
|
||||
import { useTeamAgents } from '../../hooks/useTeamAgents'
|
||||
import { AgentAvatar } from './AgentAvatar'
|
||||
import { AgentPriorityBadge, AgentWorkItemStatusBadge } from './AgentBadges'
|
||||
import { WorkItemActions } from './WorkItemActions'
|
||||
import {
|
||||
DetailFieldList,
|
||||
DetailSectionTitle,
|
||||
MessageThread,
|
||||
ProcessingStepList,
|
||||
SourceReferenceList,
|
||||
} from './WorkItemDetailSections'
|
||||
import { DetailFieldList, DetailSectionTitle, MessageThread, SourceReferenceList } from './WorkItemDetailSections'
|
||||
import { PanelLoadingState } from '../ui'
|
||||
import { AGENT_WORK_ITEM_KIND_LABELS } from '../../lib/constants'
|
||||
import { ROUTES } from '../../lib/constants'
|
||||
import { DS_BG, DS_BORDER, DS_SURFACE, DS_TEXT } from '../../lib/ds'
|
||||
import { formatTeamDateTime } from '../../lib/teamClock'
|
||||
|
||||
/** Das Anfragencenter führt den vollständigen Schriftverkehr. */
|
||||
const ANFRAGENCENTER_ROUTE = '/supply/anfragen'
|
||||
|
||||
/**
|
||||
* Detailansicht eines Vorgangs (§5.8).
|
||||
* Detailansicht eines Vorgangs.
|
||||
*
|
||||
* Als Drawer statt eigener Route: der Nutzer arbeitet eine Liste ab und will
|
||||
* nach jeder Entscheidung sofort wieder in ihr stehen — ein Seitenwechsel
|
||||
* würde bei jedem Vorgang den Kontext zerstören.
|
||||
* Bewusst schmal gehalten: Priorität, Status, Vorgangstyp, Verarbeitungsschritte
|
||||
* und Aktionshistorie sind entfallen. Sie beschrieben die Maschine, nicht den
|
||||
* Fall. Übrig bleibt, was für die Entscheidung zählt — worum es geht, was der
|
||||
* Mitarbeitende bisher hat, welche Nachricht es ausgelöst hat und woher die
|
||||
* Angaben stammen.
|
||||
*/
|
||||
export function WorkItemDetailDrawer() {
|
||||
const selectedId = useTeamStore(s => s.selectedWorkItemId)
|
||||
@@ -32,6 +31,7 @@ export function WorkItemDetailDrawer() {
|
||||
const { data: item, isLoading } = useAgentWorkItem(selectedId)
|
||||
const { data: agents = [] } = useTeamAgents()
|
||||
const sourcesRef = useRef<HTMLDivElement | null>(null)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const agent = agents.find(a => a.id === item?.agentId)
|
||||
const close = useCallback(() => setSelectedId(null), [setSelectedId])
|
||||
@@ -40,14 +40,30 @@ export function WorkItemDetailDrawer() {
|
||||
sourcesRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}, [])
|
||||
|
||||
// Vor die Callbacks gezogen: sonst steht `item` im Rumpf, aber nur
|
||||
// `item?.objectId` in den Abhängigkeiten — die Memoisierung wäre unhaltbar.
|
||||
const objectId = item?.objectId
|
||||
|
||||
// Der Objektbezug führt in «Meine Objekte» — bestehende Route, bestehende ID.
|
||||
const openProperty = useCallback(() => {
|
||||
if (!objectId) return
|
||||
navigate(ROUTES.SUPPLY.PROPERTIES, { state: { propertyId: objectId } })
|
||||
}, [navigate, objectId])
|
||||
|
||||
// Der ganze Verlauf gehört ins Anfragencenter, nicht in den Drawer.
|
||||
const openCorrespondence = useCallback(() => {
|
||||
navigate(ANFRAGENCENTER_ROUTE, { state: { objectId } })
|
||||
}, [navigate, objectId])
|
||||
|
||||
const firstMessage = item?.messageThread?.slice(0, 1) ?? []
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={!!selectedId}
|
||||
onClose={close}
|
||||
slotProps={{ paper: { sx: { width: { xs: '100%', sm: 560, lg: 640 }, display: 'flex', flexDirection: 'column' } } }}
|
||||
slotProps={{ paper: { sx: { width: { xs: '100%', sm: 520, lg: 600 }, display: 'flex', flexDirection: 'column' } } }}
|
||||
>
|
||||
{/* Kopfbereich */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -64,7 +80,7 @@ export function WorkItemDetailDrawer() {
|
||||
{agent ? `${agent.name} · ${agent.role}` : 'Vorgang'}
|
||||
</Typography>
|
||||
<Typography component="h2" sx={{ fontWeight: 700, fontSize: '1rem', color: DS_TEXT.primary, lineHeight: 1.35 }}>
|
||||
{item?.title ?? 'Vorgang wird geladen'}
|
||||
{item ? (item.objectLabel ? `${item.title} – ${item.objectLabel}` : item.title) : 'Vorgang wird geladen'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton size="small" onClick={close} aria-label="Detailansicht schliessen">
|
||||
@@ -72,32 +88,17 @@ export function WorkItemDetailDrawer() {
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Inhalt */}
|
||||
<Box sx={{ flex: 1, overflowY: 'auto', bgcolor: DS_BG.page }}>
|
||||
{isLoading && <PanelLoadingState />}
|
||||
|
||||
{item && (
|
||||
<Box sx={{ p: 2, display: 'grid', gap: 2.5 }}>
|
||||
{/* Metazeile */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||
<AgentPriorityBadge priority={item.priority} />
|
||||
<AgentWorkItemStatusBadge status={item.status} />
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.secondary }}>
|
||||
{AGENT_WORK_ITEM_KIND_LABELS[item.kind] ?? item.kind}
|
||||
</Typography>
|
||||
{item.objectId && (
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.secondary }}>
|
||||
· {item.objectId}{item.objectLabel ? ` · ${item.objectLabel}` : ''}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.muted, ml: 'auto' }}>
|
||||
{formatTeamDateTime(item.completedAt ?? item.createdAt)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.muted }}>
|
||||
{formatTeamDateTime(item.completedAt ?? item.createdAt)}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" sx={{ color: DS_TEXT.secondary }}>{item.summary}</Typography>
|
||||
|
||||
{/* Rückfrage — bei pendenten Vorgängen die wichtigste Information */}
|
||||
{item.requiresDecision && (item.escalationReason || item.decisionQuestion) && (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -110,7 +111,7 @@ export function WorkItemDetailDrawer() {
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mb: 0.75 }}>
|
||||
<HelpCircle size={15} color={DS_TEXT.warning} aria-hidden />
|
||||
<Typography sx={{ fontWeight: 700, fontSize: '0.8125rem', color: DS_TEXT.warningDark }}>
|
||||
Warum ist ein Mensch erforderlich?
|
||||
Hierzu brauche ich Ihre Entscheidung
|
||||
</Typography>
|
||||
</Box>
|
||||
{item.escalationReason && (
|
||||
@@ -133,30 +134,25 @@ export function WorkItemDetailDrawer() {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{item.messageThread && item.messageThread.length > 0 && (
|
||||
{/* Nur die auslösende Nachricht — der Rest liegt im Anfragencenter. */}
|
||||
{firstMessage.length > 0 && (
|
||||
<Box>
|
||||
<DetailSectionTitle>Nachrichtenverlauf</DetailSectionTitle>
|
||||
<MessageThread messages={item.messageThread} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{item.inputs.length > 0 && (
|
||||
<Box>
|
||||
<DetailSectionTitle>Eingabedaten</DetailSectionTitle>
|
||||
<DetailFieldList fields={item.inputs} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{item.processingSteps.length > 0 && (
|
||||
<Box>
|
||||
<DetailSectionTitle>Verarbeitungsschritte</DetailSectionTitle>
|
||||
<ProcessingStepList steps={item.processingSteps} />
|
||||
<DetailSectionTitle>Auslösende Nachricht</DetailSectionTitle>
|
||||
<MessageThread messages={firstMessage} />
|
||||
<Button
|
||||
size="small"
|
||||
endIcon={<ArrowUpRight size={14} />}
|
||||
onClick={openCorrespondence}
|
||||
sx={{ textTransform: 'none', fontWeight: 600, mt: 0.5, px: 0 }}
|
||||
>
|
||||
Gesamte Korrespondenz im Anfragencenter anzeigen
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{item.sourceReferences.length > 0 && (
|
||||
<Box ref={sourcesRef} sx={{ scrollMarginTop: 8 }}>
|
||||
<DetailSectionTitle>Fundstellen</DetailSectionTitle>
|
||||
<DetailSectionTitle>Quellen</DetailSectionTitle>
|
||||
<SourceReferenceList references={item.sourceReferences} />
|
||||
</Box>
|
||||
)}
|
||||
@@ -167,39 +163,16 @@ export function WorkItemDetailDrawer() {
|
||||
<Typography variant="body2" sx={{ color: DS_TEXT.secondary }}>{item.rejectionReason}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{item.history.length > 0 && (
|
||||
<Box>
|
||||
<DetailSectionTitle>Aktionshistorie</DetailSectionTitle>
|
||||
<Box sx={{ display: 'grid', gap: 0.75 }}>
|
||||
{item.history.map((entry) => (
|
||||
<Box key={entry.id} sx={{ display: 'flex', gap: 1, alignItems: 'baseline', flexWrap: 'wrap' }}>
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.muted, minWidth: 128 }}>
|
||||
{formatTeamDateTime(entry.at)}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: DS_TEXT.primary, fontWeight: 600 }}>
|
||||
{entry.action}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: DS_TEXT.secondary }}>
|
||||
{entry.actor}
|
||||
</Typography>
|
||||
{entry.note && (
|
||||
<>
|
||||
<Divider flexItem orientation="vertical" />
|
||||
<Typography variant="body2" sx={{ color: DS_TEXT.secondary }}>{entry.note}</Typography>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{item && item.availableActions.length > 0 && (
|
||||
<WorkItemActions item={item} onOpenSource={scrollToSources} />
|
||||
{item && (
|
||||
<WorkItemActions
|
||||
item={item}
|
||||
onShowSources={scrollToSources}
|
||||
onOpenProperty={openProperty}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user