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
@@ -1,13 +1,15 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Card, ErrorBox, IconButton, Spinner, inputClass } from '../ui/controls';
|
||||
import { Avatar, Card, ErrorBox, IconButton, Spinner, inputClass } from '../ui/controls';
|
||||
import { Modal } from '../ui/Modal';
|
||||
import { Icon } from '../ui/Icon';
|
||||
import { CountUp, Donut, BILLABLE, NON_BILLABLE } from '../ui/charts';
|
||||
import { ReportTable } from './ReportTable';
|
||||
import { EntryList } from '../booking/EntryList';
|
||||
import { ExportMenu } from './ExportMenu';
|
||||
import { CustomerReport } from './CustomerReport';
|
||||
import { ProjectReport } from './ProjectReport';
|
||||
import { useAuth } from '../../lib/auth';
|
||||
import { customersApi, qs, reportsApi, usersApi } from '../../lib/api';
|
||||
import { customersApi, entriesApi, qs, reportsApi, usersApi, type GroupRow } from '../../lib/api';
|
||||
import { useLoader, usePersistentState } from '../../lib/hooks';
|
||||
import { MONTHS, addDays, addMonths, formatDate, formatEuro, formatHours, isoWeek, mondayOf, monthEnd, monthStart, todayIso } from '../../lib/format';
|
||||
import { useRouter } from '../../lib/router';
|
||||
@@ -75,6 +77,8 @@ const TimeReport: React.FC = () => {
|
||||
const [includeFixed, setIncludeFixed] = useState(false);
|
||||
const [customers, setCustomers] = useState<Customer[]>([]);
|
||||
const [leads, setLeads] = useState<DirectoryUser[]>([]);
|
||||
// Klick auf einen Mitarbeiter oeffnet seine Buchungen des Zeitraums als Liste.
|
||||
const [detail, setDetail] = useState<GroupRow | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
customersApi.list().then(setCustomers).catch(() => undefined);
|
||||
@@ -247,14 +251,70 @@ const TimeReport: React.FC = () => {
|
||||
? (row) => navigate(`reports/kunde/${row.customer_id}`)
|
||||
: tab === 'projects'
|
||||
? (row) => navigate(`reports/projekt/${row.project_id}`)
|
||||
: undefined
|
||||
: tab === 'users'
|
||||
? (row) => setDetail(row)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{detail && (
|
||||
<UserEntriesDialog row={detail} range={range} customerId={customerId || null} onClose={() => setDetail(null)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/** Die einzelnen Buchungen eines Mitarbeiters im gewaehlten Reportzeitraum. */
|
||||
const UserEntriesDialog: React.FC<{
|
||||
row: GroupRow;
|
||||
range: { from: string; to: string; label: string };
|
||||
customerId: number | null;
|
||||
onClose: () => void;
|
||||
}> = ({ row, range, customerId, onClose }) => {
|
||||
const entries = useLoader(
|
||||
() => entriesApi.list(range.from, range.to, row.user_id, customerId),
|
||||
[range.from, range.to, row.user_id, customerId]
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
size="2xl"
|
||||
onClose={onClose}
|
||||
title={row.name}
|
||||
subtitle={
|
||||
<span className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span>{range.label}</span>
|
||||
<span className="text-ink-300">·</span>
|
||||
<span>
|
||||
{formatHours(row.minutes)} h gesamt, davon {formatHours(row.billable_minutes)} h billable
|
||||
</span>
|
||||
{row.utilization != null && (
|
||||
<>
|
||||
<span className="text-ink-300">·</span>
|
||||
<span>{row.utilization} % Auslastung</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-3 pb-4">
|
||||
<Avatar id={row.user_id} name={row.name} color={row.color} size={40} external={row.is_external} />
|
||||
<p className="text-sm font-semibold text-ink-500">
|
||||
{entries.data ? `${entries.data.length} Buchungen` : 'Lade Buchungen…'}
|
||||
</p>
|
||||
</div>
|
||||
{entries.error && <ErrorBox message={entries.error} onRetry={entries.reload} />}
|
||||
{!entries.data ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<div className="-mx-5 overflow-hidden border-t border-ink-100 sm:-mx-6">
|
||||
<EntryList entries={entries.data} emptyText="Keine Buchungen in diesem Zeitraum." />
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export { periodRange };
|
||||
|
||||
Reference in New Issue
Block a user