feat: agent-04 – Team-Dashboard für Manager/HR im Frontend angebunden
Security Audit / Python Dependency Audit (push) Has been cancelled
Security Audit / Node.js Dependency Audit (push) Has been cancelled

Das Backend liefert /dashboard/team (team_dashboard) bereits, die DashboardPage
lud aber nur /dashboard/me und /dashboard/company → Manager/HR sahen nur das
Mitarbeiter-Dashboard. Jetzt:

- Manager-Rollen (MANAGER/HR/COMPANY_ADMIN/SUPER_ADMIN) laden /dashboard/team.
- Neue Sektion "Team heute": KPIs (anwesend / abwesend / nicht eingestempelt /
  Mitarbeiterzahl), ausstehende Genehmigungen als Quick-Links, Team-Tabelle
  (Status-Badge, Einstempelzeit, Stunden heute).

Reine Frontend-Anbindung – Backend, Reports und Exports (PDF/CSV/XLSX) waren
bereits vollständig (24 Report-Tests grün).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 13:27:48 +02:00
co-authored by Claude Opus 4.8
parent 6a34bd11d8
commit e1d423e634
+95
View File
@@ -49,6 +49,25 @@ interface CompanyDashboard {
upcoming_absences: UpcomingAbsence[] upcoming_absences: UpcomingAbsence[]
} }
interface TeamMemberStatus {
user_id: string
user_name: string
department: string | null
status: string // "present" | "on_leave" | "absent"
absence_type: string | null
time_in: string | null
hours_today: number | null
}
interface TeamDashboard {
present_count: number
on_leave_count: number
absent_count: number
pending_time_approvals: number
pending_absence_approvals: number
members: TeamMemberStatus[]
}
function StatCard({ function StatCard({
label, value, sub, highlight, label, value, sub, highlight,
}: { }: {
@@ -75,6 +94,7 @@ export function DashboardPage() {
const { logout } = useAuth() const { logout } = useAuth()
const [user, setUser] = useState<UserOut | null>(null) const [user, setUser] = useState<UserOut | null>(null)
const [emp, setEmp] = useState<EmployeeDashboard | null>(null) const [emp, setEmp] = useState<EmployeeDashboard | null>(null)
const [team, setTeam] = useState<TeamDashboard | null>(null)
const [comp, setComp] = useState<CompanyDashboard | null>(null) const [comp, setComp] = useState<CompanyDashboard | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState('') const [error, setError] = useState('')
@@ -89,7 +109,15 @@ export function DashboardPage() {
setUser(me) setUser(me)
setEmp(empDash) setEmp(empDash)
const managerRoles = ['MANAGER', 'HR', 'COMPANY_ADMIN', 'SUPER_ADMIN']
const adminRoles = ['COMPANY_ADMIN', 'SUPER_ADMIN'] const adminRoles = ['COMPANY_ADMIN', 'SUPER_ADMIN']
if (managerRoles.includes(me.role)) {
try {
setTeam(await api.get<TeamDashboard>('/dashboard/team'))
} catch {
// kein Manager-Zugriff still ignorieren
}
}
if (adminRoles.includes(me.role)) { if (adminRoles.includes(me.role)) {
try { try {
setComp(await api.get<CompanyDashboard>('/dashboard/company')) setComp(await api.get<CompanyDashboard>('/dashboard/company'))
@@ -203,6 +231,73 @@ export function DashboardPage() {
</section> </section>
)} )}
{/* Team-Dashboard (Manager/HR) */}
{team && (
<section>
<h2 className='text-base font-semibold text-gray-700 mb-3'>Team heute</h2>
<div className='grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4'>
<StatCard label='Anwesend' value={team.present_count} highlight='green' />
<StatCard label='Abwesend (Urlaub/Krank)' value={team.on_leave_count} />
<StatCard label='Nicht eingestempelt' value={team.absent_count}
highlight={team.absent_count > 0 ? 'yellow' : undefined} />
<StatCard label='Mitarbeiter' value={team.members.length} />
</div>
{/* Ausstehende Genehmigungen */}
{(team.pending_time_approvals > 0 || team.pending_absence_approvals > 0) && (
<div className='flex flex-wrap gap-2 mb-4'>
{team.pending_time_approvals > 0 && (
<Link to='/time?status=pending' className='bg-yellow-100 text-yellow-800 text-xs px-3 py-1.5 rounded-full font-medium hover:bg-yellow-200 transition-colors'>
{team.pending_time_approvals} Zeiteinträge warten auf Genehmigung
</Link>
)}
{team.pending_absence_approvals > 0 && (
<Link to='/absences?status=pending' className='bg-yellow-100 text-yellow-800 text-xs px-3 py-1.5 rounded-full font-medium hover:bg-yellow-200 transition-colors'>
{team.pending_absence_approvals} Abwesenheitsanträge warten auf Genehmigung
</Link>
)}
</div>
)}
{team.members.length > 0 && (
<div className='bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden'>
<div className='overflow-x-auto'>
<table className='w-full text-sm'>
<thead className='bg-gray-50 text-gray-500 text-xs uppercase'>
<tr>
{['Mitarbeiter', 'Abteilung', 'Status', 'Seit', 'Heute'].map(h => (
<th key={h} className='px-4 py-2.5 text-left font-medium'>{h}</th>
))}
</tr>
</thead>
<tbody className='divide-y divide-gray-100'>
{team.members.map(m => {
const badge = m.status === 'present'
? { c: 'bg-green-100 text-green-700', t: 'Anwesend' }
: m.status === 'on_leave'
? { c: 'bg-blue-100 text-blue-700', t: m.absence_type ?? 'Abwesend' }
: { c: 'bg-gray-100 text-gray-500', t: 'Nicht da' }
return (
<tr key={m.user_id} className='hover:bg-gray-50 transition-colors'>
<td className='px-4 py-2.5 font-medium text-gray-800'>{m.user_name}</td>
<td className='px-4 py-2.5 text-gray-500'>{m.department ?? '—'}</td>
<td className='px-4 py-2.5'>
<span className={`inline-flex px-2 py-0.5 rounded-full text-xs font-medium ${badge.c}`}>{badge.t}</span>
</td>
<td className='px-4 py-2.5 text-gray-600 font-mono text-xs'>{m.time_in?.slice(0, 5) ?? '—'}</td>
<td className='px-4 py-2.5 text-gray-600'>{m.hours_today != null ? `${m.hours_today.toFixed(1)} h` : '—'}</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
)}
</section>
)}
{/* Unternehmens-Dashboard (nur Admin) */} {/* Unternehmens-Dashboard (nur Admin) */}
{comp && ( {comp && (
<section> <section>