feat(team): Oberfläche der Teamübersicht
Vier Seiten, eingebettet in die bestehende App-Shell — keine zweite Sidebar, keine zweite Kopfzeile, kein eigenes Property-On-Branding: - Teamübersicht: Auswertung (drei Kennzahlen, gemeinsam filterbar nach Zeitraum, Bereich und Mitarbeiter), Kernteam, Kanäle & Systeme — in dieser Reihenfolge - Bearbeitungsverlauf: zwei Reiter, neun Filterdimensionen, Detail-Drawer mit Ergebnis, Eingabedaten, Verarbeitungsschritten, Fundstellen, Nachrichtenverlauf und Aktionshistorie; Freigeben, Anpassen, Zurückweisen, Rückfrage beantworten - Personalverwaltung: dreigeteilt (Hauptnavigation, Agentenliste, Dossier) mit fünf Reitern — Aufgaben, Kanäle, Systeme, Einstellungen, Protokoll - Kanäle & Systeme: neun Verbindungen, mehrstufiger Assistent, ERP-Auswahl, deterministischer Verbindungstest Navigation: NavItem um `children` erweitert, "Teamübersicht" steht exakt zwischen "Meine Objekte" und "Reminder Manager", die drei Subreiter bleiben eingerückt sichtbar, solange man im Funktionsbereich ist. Subreiter sind echte Routen, keine lokalen Tabs. Die App kannte bisher keine Feature-Route mit eigenem Outlet und baut Tabs sonst über lokalen useState — hier trug das nicht: die Subreiter müssen in der Sidebar stehen und deep-linkbar sein. Die Routen sind flache Geschwister, kein Outlet-Baum. Freigabepflichtige Aktionen laufen über einen Bestätigungsdialog mit sichtbarer Konsequenz; die 1-Klick-Bestätigung ist die fachlich vorgesehene Ausnahme. Jeder Status trägt neben der Farbe immer einen Text. Null rohe Hex-Werte: check:tokens bleibt unverändert bei 2249. Der in der Spezifikation genannte dunkelblaue Banner "Heute geleistet …" existiert in dieser Codebasis nicht — er wird deshalb auch nicht erzeugt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import { useState } from 'react'
|
||||
import { Box, FormControlLabel, Switch, Typography } from '@mui/material'
|
||||
import { AlertTriangle, Mail } from 'lucide-react'
|
||||
import type { TeamAgent } from '../../domain/teamAgent'
|
||||
import { AgentStatus } from '../../domain/teamAgent'
|
||||
import { AgentAvatar } from './AgentAvatar'
|
||||
import { AgentAutonomyBadge, AgentStatusBadge } from './AgentBadges'
|
||||
import { ConfirmDialog } from '../ui'
|
||||
import { useSetAgentActive } from '../../hooks/useTeamAgents'
|
||||
import { DS_BG, DS_BORDER, DS_SURFACE, DS_TEXT } from '../../lib/ds'
|
||||
import { formatTeamDateTime } from '../../lib/teamClock'
|
||||
|
||||
interface Props {
|
||||
agent: TeamAgent
|
||||
}
|
||||
|
||||
function MetaField({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.muted, display: 'block' }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: DS_TEXT.primary, fontWeight: 600 }}>
|
||||
{value}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kopfbereich des Personalblatts (§7.3).
|
||||
*
|
||||
* Das Pausieren läuft über einen Bestätigungsdialog mit Hinweis auf die
|
||||
* betroffenen Aufgaben — ein versehentlich pausierter Fristen-Wächter würde
|
||||
* echte Termine verpassen (§7.5, §17.3).
|
||||
*/
|
||||
export function AgentDossierHeader({ agent }: Props) {
|
||||
const [confirmPause, setConfirmPause] = useState(false)
|
||||
const setActive = useSetAgentActive()
|
||||
const isActive = agent.status === AgentStatus.ACTIVE
|
||||
const enabledTasks = agent.tasks.filter(t => t.enabled).length
|
||||
|
||||
const handleToggle = (next: boolean) => {
|
||||
if (next) setActive.mutate({ id: agent.id, active: true })
|
||||
else setConfirmPause(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ px: 3, pt: 2.5, pb: 2, bgcolor: DS_BG.surface, borderBottom: `1px solid ${DS_BORDER.default}` }}>
|
||||
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
||||
<AgentAvatar agent={agent} size="large" showStatus />
|
||||
|
||||
<Box sx={{ flex: 1, minWidth: 240 }}>
|
||||
<Typography component="h2" sx={{ fontWeight: 700, fontSize: '1.25rem', color: DS_TEXT.primary, lineHeight: 1.25 }}>
|
||||
{agent.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: DS_TEXT.secondary }}>
|
||||
{agent.role}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
|
||||
<Mail size={13} color={DS_TEXT.muted} aria-hidden />
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.secondary }}>{agent.email}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap', mt: 1 }}>
|
||||
<AgentStatusBadge status={agent.status} />
|
||||
<AgentAutonomyBadge autonomy={agent.autonomyLevel} note={agent.autonomyNote} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={isActive}
|
||||
disabled={setActive.isPending}
|
||||
onChange={(e) => handleToggle(e.target.checked)}
|
||||
slotProps={{ input: { 'aria-label': `${agent.name} aktiv` } }}
|
||||
/>
|
||||
}
|
||||
label="Agent aktiv"
|
||||
sx={{ mr: 0, '& .MuiFormControlLabel-label': { fontWeight: 600, fontSize: '0.875rem' } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Stammdaten */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gap: 1.5,
|
||||
gridTemplateColumns: { xs: 'repeat(2, 1fr)', md: 'repeat(4, 1fr)' },
|
||||
mt: 2,
|
||||
pt: 2,
|
||||
borderTop: `1px solid ${DS_BORDER.muted}`,
|
||||
}}
|
||||
>
|
||||
<MetaField label="Personalnummer" value={agent.personnelNumber} />
|
||||
<MetaField label="Abteilung" value={agent.department} />
|
||||
<MetaField label="Autonomiegrad" value={agent.autonomyNote} />
|
||||
<MetaField
|
||||
label="Letzter Lauf"
|
||||
value={agent.lastRun ? formatTeamDateTime(agent.lastRun) : 'Noch nicht gelaufen'}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Warnung im Personalblatt, solange pausiert (§7.5) */}
|
||||
{!isActive && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
alignItems: 'flex-start',
|
||||
mt: 2,
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
bgcolor: DS_SURFACE.warning.bg,
|
||||
border: `1px solid ${DS_SURFACE.warning.border}`,
|
||||
}}
|
||||
>
|
||||
<AlertTriangle size={16} color={DS_TEXT.warning} aria-hidden style={{ flexShrink: 0, marginTop: 1 }} />
|
||||
<Typography variant="body2" sx={{ color: DS_TEXT.warningDark }}>
|
||||
{agent.name} ist pausiert. Bis zur Reaktivierung werden keine Aufgaben ausgeführt und es
|
||||
entstehen keine neuen Vorgänge.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmPause}
|
||||
destructive
|
||||
title={`${agent.name} pausieren?`}
|
||||
message={`${enabledTasks} aktive Aufgaben werden ausgesetzt, bis Sie ${agent.name} wieder aktivieren. Laufende Fristen und Anfragen werden in dieser Zeit nicht bearbeitet. Der Vorgang wird protokolliert.`}
|
||||
confirmLabel="Pausieren"
|
||||
onConfirm={() => {
|
||||
setActive.mutate({ id: agent.id, active: false })
|
||||
setConfirmPause(false)
|
||||
}}
|
||||
onCancel={() => setConfirmPause(false)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user