feat(ansichten): Listenansicht fuer Buchungen, Report und Kunden

Neue Komponente EntryList zeigt einzelne Buchungen als sortierbare Tabelle
mit Datum, Kunde, Projekt, Task und der Zeit getrennt nach billable und
non-billable, inklusive Summen.

- Buchung: Umschalter Kalender/Liste neben Woche/Monat/Jahr, Auswahl wird
  im Browser gemerkt. Klick auf eine Zeile oeffnet die Buchung.
- Reports: Klick auf einen Mitarbeiter oeffnet dessen Buchungen des
  Reportzeitraums als Liste; ein gesetzter Kundenfilter wird uebernommen.
- Kunden: Umschalter Karten/Liste mit Ort, Projekten, Stunden und Umsatz.
- GET /api/entries akzeptiert dafuer customer_id, Segmented nimmt Icons,
  Modal kennt die Groesse 2xl.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-18 10:14:48 +02:00
co-authored by Claude Opus 5
parent 8c93df11d9
commit ef7171496f
9 changed files with 424 additions and 17 deletions
+27 -5
View File
@@ -7,6 +7,7 @@ import { ExportDialog } from './ExportDialog';
import { WeekView } from './WeekView';
import { MonthView } from './MonthView';
import { YearView } from './YearView';
import { EntryList } from './EntryList';
import { useAuth } from '../../lib/auth';
import { entriesApi, projectsApi, usersApi } from '../../lib/api';
import { useIsMobile, useLoader, usePersistentState, useCountUp } from '../../lib/hooks';
@@ -27,6 +28,8 @@ import { visibleTeam } from '../team/hierarchy';
import { atLeast, type DirectoryUser, type Project, type TimeEntry } from '../../types';
export type BookingMode = 'week' | 'month' | 'year';
/** Kalender = Wochen-/Monats-/Jahresraster, Liste = die einzelnen Buchungen als Tabelle. */
export type BookingLayout = 'calendar' | 'list';
function rangeFor(mode: BookingMode, anchor: string) {
if (mode === 'week') {
@@ -48,6 +51,7 @@ export const Booking: React.FC = () => {
const { user, settings } = useAuth();
const isMobile = useIsMobile();
const [mode, setMode] = usePersistentState<BookingMode>('pulse_booking_mode', 'week');
const [layout, setLayout] = usePersistentState<BookingLayout>('pulse_booking_layout', 'calendar');
const [anchor, setAnchor] = useState(todayIso());
const [modal, setModal] = useState<{ date: string; entry?: TimeEntry | null } | null>(null);
const [exportOpen, setExportOpen] = useState(false);
@@ -141,7 +145,7 @@ export const Booking: React.FC = () => {
</div>
<h2 className="tabular min-w-0 flex-1 truncate text-base text-ink-900 sm:flex-none sm:text-lg">{label}</h2>
<div className="order-last flex w-full items-center gap-2 sm:order-none sm:ml-auto sm:w-auto">
<div className="order-last flex w-full flex-wrap items-center gap-2 sm:order-none sm:ml-auto sm:w-auto sm:flex-nowrap">
<Segmented
value={mode}
onChange={setMode}
@@ -152,6 +156,15 @@ export const Booking: React.FC = () => {
{ value: 'year', label: 'Jahr' },
]}
/>
<Segmented
value={layout}
onChange={setLayout}
className="flex-1 sm:flex-none"
options={[
{ value: 'calendar', label: 'Kalender', icon: 'calendar', title: 'Kalenderansicht' },
{ value: 'list', label: 'Liste', icon: 'list', title: 'Alle Buchungen als Liste' },
]}
/>
{isLead && team.length > 1 && (
<select className={`${inputClass} w-auto max-w-[180px] py-2 text-xs font-bold`} value={viewUserId} onChange={(e) => setViewUserId(Number(e.target.value))} title="Zeiten ansehen von">
{team.map((t) => (
@@ -207,11 +220,20 @@ export const Booking: React.FC = () => {
{entries.loading && !entries.data ? (
<Spinner />
) : (
<div key={`${mode}-${range.from}`} className="animate-fade">
{mode === 'week' && (
<div key={`${layout}-${mode}-${range.from}`} className="animate-fade">
{layout === 'list' && (
<Card className="overflow-hidden">
<EntryList
entries={list}
onEdit={readOnly ? undefined : (entry) => setModal({ date: entry.entry_date, entry })}
emptyText={`Keine Buchungen ${mode === 'week' ? 'in dieser Woche' : mode === 'month' ? 'in diesem Monat' : 'in diesem Jahr'}.`}
/>
</Card>
)}
{layout === 'calendar' && mode === 'week' && (
<WeekView from={range.from} entries={list} readOnly={readOnly} dailyTarget={(weekly / 5) * 60} onAdd={openNew} onEdit={(entry) => !readOnly && setModal({ date: entry.entry_date, entry })} />
)}
{mode === 'month' && (
{layout === 'calendar' && mode === 'month' && (
<MonthView
month={range.from}
entries={list}
@@ -224,7 +246,7 @@ export const Booking: React.FC = () => {
readOnly={readOnly}
/>
)}
{mode === 'year' && (
{layout === 'calendar' && mode === 'year' && (
<YearView
year={anchor.slice(0, 4)}
entries={list}