feat: perinco Pulse - Zeiterfassung fuer perinco
Erste Fassung, live unter https://pulse.perinco.de. Frontend React 19 + TypeScript + Tailwind 4 (Vite), Backend Express mit mysql2 auf MariaDB, Exporte als PDF (pdfkit), Excel (exceljs) und CSV. Aufbau, Designsystem und die sieben Themes kommen aus dem perinco CRM. Fachlich: - Buchung mit Wochen-, Monats- und Jahresansicht, Zeiten als 1:23 oder 1,5 - Projekte mit n Budgettoepfen: Laufzeit, Volumen, Sperre bei Ablauf oder aufgebrauchtem Budget, Buchung nur auf den passenden Topf - Kunden, Team mit Hierarchie, Reports im Harvest-Stil, Monatsexporte - Rollen user/lead/management/admin, doppelt geprueft (Menue und Backend) - SSO gegen Microsoft Entra ID, Uebernahme der AD-Benutzer per Drag & Drop Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '../../lib/theme';
|
||||
import { THEME_PALETTES, THEME_PALETTE_LABELS } from '../../types';
|
||||
import { Icon } from '../ui/Icon';
|
||||
import { PulseMark } from './PulseMark';
|
||||
|
||||
interface HeaderProps {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
onToggleSidebar: () => void;
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({ title, subtitle, onToggleSidebar, isMobile }) => {
|
||||
const { theme, preference, toggle, useSystem, palette, setPalette } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
return (
|
||||
<header className="app-header relative flex shrink-0 items-center gap-3 border-b border-ink-200 bg-surface px-4 py-3 md:px-6 md:py-4">
|
||||
{isMobile ? (
|
||||
<PulseMark size={34} />
|
||||
) : (
|
||||
<button
|
||||
onClick={onToggleSidebar}
|
||||
className="rounded-lg p-2 text-ink-500 transition hover:bg-ink-100 hover:text-ink-800"
|
||||
title="Menü ein-/ausklappen"
|
||||
aria-label="Menü ein-/ausklappen"
|
||||
>
|
||||
<Icon name="menu" strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="truncate text-lg leading-tight text-ink-900">{title}</h1>
|
||||
{subtitle && <p className="truncate text-xs font-semibold text-ink-400">{subtitle}</p>}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<label className="sr-only" htmlFor="theme-palette">
|
||||
Design
|
||||
</label>
|
||||
<select
|
||||
id="theme-palette"
|
||||
value={palette}
|
||||
onChange={(event) => setPalette(event.target.value as typeof palette)}
|
||||
title={`Design: ${THEME_PALETTE_LABELS[palette].hint}`}
|
||||
className="max-w-[110px] rounded-xl border border-ink-200 bg-surface px-2.5 py-2 text-xs font-bold text-ink-600 outline-none transition hover:border-brand-300 hover:text-brand-700 focus:border-brand-500 sm:max-w-none sm:px-3"
|
||||
>
|
||||
{THEME_PALETTES.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{THEME_PALETTE_LABELS[option].name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={toggle}
|
||||
onDoubleClick={useSystem}
|
||||
title={`${isDark ? 'Helles' : 'Dunkles'} Design${preference === null ? ' (folgt dem System)' : ' · Doppelklick: dem System folgen'}`}
|
||||
aria-label={`${isDark ? 'Helles' : 'Dunkles'} Design einschalten`}
|
||||
className="flex items-center gap-2 rounded-xl border border-ink-200 px-2.5 py-2 text-xs font-bold text-ink-600 transition hover:border-brand-300 hover:text-brand-700 sm:px-3"
|
||||
>
|
||||
<Icon name={isDark ? 'sun' : 'moon'} className="h-4 w-4" />
|
||||
<span className="hidden lg:inline">{isDark ? 'Hell' : 'Dunkel'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user