Neuer Menuepunkt "Kalender" fuer alle Rollen: genehmigte Urlaube aller Mitarbeiter, auch in der Zukunft, dazu Feiertage. Monatsansicht (Standard) und Zeitleiste, filterbar nach Team und Name. - Eigene Tabelle pulse_calendar_days, gefuellt von lib/calendarSync.js im HRworks-Lauf (Fenster bis 400 Tage voraus). Buchungen entstehen weiter erst am jeweiligen Tag. - Nur validatedOk und nur freigegebene Arten (calendar_types, Haken in den HRworks-Einstellungen). Krankmeldungen werden dafuer nicht abgefragt. - GET /api/calendar fuer alle Angemeldeten, ohne Mails und Profilbilder. - Timer laeuft zusaetzlich um 12:00 Uhr. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116 lines
4.8 KiB
TypeScript
116 lines
4.8 KiB
TypeScript
import React from 'react';
|
|
/*
|
|
* Ueber den Import statt aus public/: Vite haengt einen Hash an den Dateinamen.
|
|
* Apache laesst Bilder ein Jahr im Browser-Cache liegen - bei gleichem Namen
|
|
* sieht man eine neue Fassung sonst erst nach einem harten Neuladen.
|
|
*/
|
|
import pulseLogo from '../../assets/pulse-logo.png';
|
|
import { Icon, type IconName } from '../ui/Icon';
|
|
import { Avatar } from '../ui/controls';
|
|
import { PulseMark } from './PulseMark';
|
|
import { useAuth } from '../../lib/auth';
|
|
import { ROLE_LABELS, VIEW_LABELS, homeFor, viewsFor, type ViewKey } from '../../types';
|
|
|
|
export const VIEW_ICONS: Record<ViewKey, IconName> = {
|
|
dashboard: 'dashboard',
|
|
buchung: 'clock',
|
|
projekte: 'folder',
|
|
kalender: 'calendar',
|
|
team: 'team',
|
|
kunden: 'building',
|
|
reports: 'chart',
|
|
einstellungen: 'settings',
|
|
};
|
|
|
|
interface SidebarProps {
|
|
current: ViewKey;
|
|
onNavigate: (view: ViewKey) => void;
|
|
collapsed: boolean;
|
|
onAccount: () => void;
|
|
}
|
|
|
|
/** Seitenleiste fuer Desktop/Tablet. Auf dem Handy uebernimmt die MobileNav. */
|
|
export const Sidebar: React.FC<SidebarProps> = ({ current, onNavigate, collapsed, onAccount }) => {
|
|
const { user, logout } = useAuth();
|
|
if (!user) return null;
|
|
const views = viewsFor(user.role);
|
|
const main = views.filter((v) => v !== 'einstellungen');
|
|
const home = homeFor(user.role);
|
|
|
|
const item = (view: ViewKey) => {
|
|
const active = current === view;
|
|
return (
|
|
<button
|
|
key={view}
|
|
onClick={() => onNavigate(view)}
|
|
title={collapsed ? VIEW_LABELS[view] : undefined}
|
|
aria-current={active ? 'page' : undefined}
|
|
className={`group relative flex w-full items-center rounded-xl transition ${collapsed ? 'justify-center py-2.5' : 'gap-3 px-3 py-2.5'} ${
|
|
active ? 'bg-brand-500 text-white shadow-card' : 'text-ink-600 hover:bg-brand-50 hover:text-brand-700'
|
|
}`}
|
|
>
|
|
<Icon name={VIEW_ICONS[view]} className="h-5 w-5 shrink-0" strokeWidth={active ? 2.2 : 1.7} />
|
|
{!collapsed && <span className="truncate text-sm font-bold">{VIEW_LABELS[view]}</span>}
|
|
</button>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<aside
|
|
className={`app-sidebar hidden h-full shrink-0 flex-col overflow-hidden border-r border-ink-200 bg-surface transition-[width] duration-300 md:flex ${
|
|
collapsed ? 'w-[76px]' : 'w-64'
|
|
}`}
|
|
>
|
|
<button
|
|
onClick={() => onNavigate(home)}
|
|
className={`flex h-[var(--app-bar-height)] shrink-0 items-center border-b border-ink-200 transition hover:bg-ink-50 ${
|
|
collapsed ? 'justify-center' : 'gap-3 px-5'
|
|
}`}
|
|
title="Zur Startseite"
|
|
>
|
|
{collapsed ? (
|
|
<PulseMark size={34} />
|
|
) : (
|
|
/*
|
|
* Das Pulse-Logo traegt den Namen selbst, deshalb steht daneben kein
|
|
* Schriftzug mehr. Kein logo-adaptive: die Wuerfel sind gruen und
|
|
* sollen im Dunkelmodus nicht umgefaerbt werden.
|
|
*/
|
|
<img src={pulseLogo} alt="Pulse" className="max-h-[46px] w-auto max-w-full object-contain" />
|
|
)}
|
|
</button>
|
|
|
|
<nav className="scrollbar-slim flex-1 space-y-1 overflow-y-auto px-3 py-4">
|
|
{!collapsed && <p className="mb-2 px-3 text-[10px] font-extrabold uppercase tracking-[0.18em] text-ink-400">Navigation</p>}
|
|
{main.map(item)}
|
|
</nav>
|
|
|
|
{views.includes('einstellungen') && <div className="shrink-0 border-t border-ink-100 px-3 py-3">{item('einstellungen')}</div>}
|
|
|
|
<div className={`shrink-0 border-t border-ink-100 ${collapsed ? 'flex flex-col items-center gap-2 p-3' : 'p-4'}`}>
|
|
<div className="flex w-full min-w-0 items-center justify-between gap-2">
|
|
<button onClick={onAccount} className="flex min-w-0 items-center gap-3 rounded-xl text-left transition hover:opacity-80" title="Mein Konto">
|
|
<Avatar name={user.name} color={user.color} avatar={user.avatar} id={user.id} size={36} external={user.is_external} />
|
|
{!collapsed && (
|
|
<span className="min-w-0">
|
|
<span className="block truncate text-sm font-bold text-ink-900">{user.name}</span>
|
|
<span className="block truncate text-xs text-ink-400">{ROLE_LABELS[user.role]}</span>
|
|
</span>
|
|
)}
|
|
</button>
|
|
{!collapsed && (
|
|
<button onClick={logout} title="Abmelden" className="shrink-0 rounded-lg p-2 text-ink-400 transition hover:bg-red-50 hover:text-red-600">
|
|
<Icon name="logout" className="h-4 w-4" strokeWidth={2} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
{collapsed && (
|
|
<button onClick={logout} title="Abmelden" className="rounded-lg p-2 text-ink-400 transition hover:bg-red-50 hover:text-red-600">
|
|
<Icon name="logout" className="h-4 w-4" strokeWidth={2} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
);
|
|
};
|