feat: agent-04 – Team-Dashboard für Manager/HR im Frontend angebunden
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:
@@ -49,6 +49,25 @@ interface CompanyDashboard {
|
||||
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({
|
||||
label, value, sub, highlight,
|
||||
}: {
|
||||
@@ -75,6 +94,7 @@ export function DashboardPage() {
|
||||
const { logout } = useAuth()
|
||||
const [user, setUser] = useState<UserOut | null>(null)
|
||||
const [emp, setEmp] = useState<EmployeeDashboard | null>(null)
|
||||
const [team, setTeam] = useState<TeamDashboard | null>(null)
|
||||
const [comp, setComp] = useState<CompanyDashboard | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
@@ -89,7 +109,15 @@ export function DashboardPage() {
|
||||
setUser(me)
|
||||
setEmp(empDash)
|
||||
|
||||
const managerRoles = ['MANAGER', 'HR', '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)) {
|
||||
try {
|
||||
setComp(await api.get<CompanyDashboard>('/dashboard/company'))
|
||||
@@ -203,6 +231,73 @@ export function DashboardPage() {
|
||||
</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) */}
|
||||
{comp && (
|
||||
<section>
|
||||
|
||||
Reference in New Issue
Block a user