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:
co-authored by
Claude Opus 5
parent
8c93df11d9
commit
ef7171496f
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user