feat(kalender): Kalender mit Urlauben und Feiertagen fuer alle
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6ae5a01bda
commit
c0eb00faee
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { WEEKDAYS_SHORT, addDays, fromIso, monthEnd, todayIso, weekdayIndex } from '../../lib/format';
|
||||
import { Avatar } from '../ui/controls';
|
||||
import type { CalendarUser } from '../../lib/api';
|
||||
import { holidaysOn, userColor, type CalendarIndex } from './calendarData';
|
||||
|
||||
/**
|
||||
* Zeitleiste: Personen in Zeilen, Tage des Monats in Spalten. Zeigt auf einen
|
||||
* Blick, wo sich Urlaube ueberschneiden. Breiter als das Handy - die Tabelle
|
||||
* scrollt seitlich, die Namensspalte bleibt stehen.
|
||||
*/
|
||||
export const Timeline: React.FC<{
|
||||
month: string;
|
||||
index: CalendarIndex;
|
||||
users: CalendarUser[];
|
||||
selected: string | null;
|
||||
onSelect: (day: string) => void;
|
||||
}> = ({ month, index, users, selected, onSelect }) => {
|
||||
const today = todayIso();
|
||||
const days: string[] = [];
|
||||
for (let d = month; d <= monthEnd(month); d = addDays(d, 1)) days.push(d);
|
||||
|
||||
return (
|
||||
<div className="scrollbar-slim overflow-x-auto rounded-2xl border border-ink-200 bg-surface shadow-card">
|
||||
<table className="w-full border-separate border-spacing-0 text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="sticky left-0 z-20 min-w-[128px] border-b border-ink-200 bg-surface px-3 py-2 text-left text-[11px] font-extrabold uppercase tracking-wider text-ink-400 sm:min-w-[180px]">
|
||||
Mitarbeiter
|
||||
</th>
|
||||
{days.map((day) => {
|
||||
const weekend = weekdayIndex(day) >= 5;
|
||||
const holidays = holidaysOn(index, day);
|
||||
return (
|
||||
<th
|
||||
key={day}
|
||||
title={holidays.join(', ') || undefined}
|
||||
onClick={() => onSelect(day)}
|
||||
className={`min-w-[28px] cursor-pointer border-b border-ink-200 px-0 py-1.5 text-center font-extrabold ${
|
||||
day === selected ? 'bg-brand-100 text-brand-700' : holidays.length ? 'bg-amber-50 text-amber-700' : weekend ? 'bg-ink-50 text-ink-400' : 'text-ink-600'
|
||||
}`}
|
||||
>
|
||||
<span className="block text-[9px] font-bold uppercase text-ink-400">{WEEKDAYS_SHORT[weekdayIndex(day)].slice(0, 2)}</span>
|
||||
<span className={day === today ? 'rounded-full bg-brand-500 px-1 text-white' : ''}>{fromIso(day).getDate()}</span>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user) => {
|
||||
const entries = index.byUser.get(user.id);
|
||||
const color = userColor(user);
|
||||
return (
|
||||
<tr key={user.id} className="group">
|
||||
<td className="sticky left-0 z-10 border-b border-ink-100 bg-surface px-3 py-1.5 group-hover:bg-ink-50">
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<Avatar id={user.id} name={user.name} color={user.color} size={22} external={user.is_external} />
|
||||
<span className="max-w-[92px] truncate font-bold text-ink-800 sm:max-w-[140px]">{user.name}</span>
|
||||
</span>
|
||||
</td>
|
||||
{days.map((day) => {
|
||||
const entry = entries?.get(day);
|
||||
const weekend = weekdayIndex(day) >= 5;
|
||||
const vacation = entry?.kind === 'vacation';
|
||||
// Aneinanderstossende Urlaubstage als ein Balken zeichnen.
|
||||
const prev = entries?.get(addDays(day, -1))?.kind === 'vacation';
|
||||
const next = entries?.get(addDays(day, 1))?.kind === 'vacation';
|
||||
return (
|
||||
<td
|
||||
key={day}
|
||||
title={entry ? `${user.name}: ${entry.label}${entry.half ? ' (halber Tag)' : ''}` : undefined}
|
||||
onClick={() => onSelect(day)}
|
||||
className={`h-9 cursor-pointer border-b border-ink-100 px-0 py-1 ${
|
||||
day === selected ? 'bg-brand-50' : entry?.kind === 'holiday' ? 'bg-amber-50' : weekend ? 'bg-ink-50/70' : ''
|
||||
}`}
|
||||
>
|
||||
{vacation && (
|
||||
<span
|
||||
className={`block h-5 ${prev ? '' : 'ml-0.5 rounded-l-md'} ${next ? '' : 'mr-0.5 rounded-r-md'}`}
|
||||
style={{
|
||||
background: entry.half ? `linear-gradient(to top, ${color} 50%, color-mix(in srgb, ${color} 25%, transparent) 50%)` : color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{entry?.kind === 'holiday' && <span className="mx-auto block h-1.5 w-1.5 rounded-full bg-amber-400" />}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user