feat(kunden): Filter "Inaktiv" in Kundenliste und Kundenreport
Mit den 25 stillgelegten Kunden aus der Harvest-Historie war "Aktiv" oder "Alle" zu grob: wer nachsehen will, wen wir stillgelegt haben, musste ihn zwischen allen 63 suchen. Kundenliste und Kundenreport haben jetzt drei Stufen - Aktiv, Inaktiv, Alle. Im Kundenreport bleibt der gewaehlte Kunde in der Auswahl stehen, auch wenn der Filter ihn sonst ausblenden wuerde; stillgelegte sind mit "(inaktiv)" gekennzeichnet. Im Zeitreport stehen sie unter "Inaktiv" in einer eigenen Gruppe, damit sie die Auswahl nicht zuwachsen lassen - die Historie bleibt ja auswertbar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e908db2788
commit
0cd426777d
@@ -17,7 +17,7 @@ export const Customers: React.FC = () => {
|
||||
const { navigate } = useRouter();
|
||||
const { addToast } = useToast();
|
||||
const [search, setSearch] = useState('');
|
||||
const [status, setStatus] = useState<'active' | 'all'>('active');
|
||||
const [status, setStatus] = useState<'active' | 'inactive' | 'all'>('active');
|
||||
const [form, setForm] = useState<{ customer: Customer | null } | null>(null);
|
||||
const [remove, setRemove] = useState<CustomerOverview | null>(null);
|
||||
const [expanded, setExpanded] = useState<number | null>(null);
|
||||
@@ -26,7 +26,7 @@ export const Customers: React.FC = () => {
|
||||
const customers = useMemo(() => {
|
||||
const q = search.trim().toLowerCase();
|
||||
return (data?.customers ?? [])
|
||||
.filter((c) => (status === 'all' || c.is_active) && (!q || `${c.name} ${c.short_name} ${c.city ?? ''}`.toLowerCase().includes(q)))
|
||||
.filter((c) => (status === 'all' || (status === 'inactive' ? !c.is_active : c.is_active)) && (!q || `${c.name} ${c.short_name} ${c.city ?? ''}`.toLowerCase().includes(q)))
|
||||
.sort((a, b) => b.earned + b.open - (a.earned + a.open) || a.name.localeCompare(b.name, 'de'));
|
||||
}, [data, search, status]);
|
||||
|
||||
@@ -74,7 +74,16 @@ export const Customers: React.FC = () => {
|
||||
<Icon name="search" className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-ink-400" />
|
||||
<input className={`${inputClass} pl-9`} placeholder="Kunde suchen…" value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
</div>
|
||||
<Segmented size="sm" value={status} onChange={setStatus} options={[{ value: 'active', label: 'Aktiv' }, { value: 'all', label: 'Alle' }]} />
|
||||
<Segmented
|
||||
size="sm"
|
||||
value={status}
|
||||
onChange={setStatus}
|
||||
options={[
|
||||
{ value: 'active', label: 'Aktiv' },
|
||||
{ value: 'inactive', label: 'Inaktiv', title: 'Kunden ohne laufende Zusammenarbeit' },
|
||||
{ value: 'all', label: 'Alle' },
|
||||
]}
|
||||
/>
|
||||
<Segmented
|
||||
size="sm"
|
||||
value={layout}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Badge, BudgetBar, Card, EmptyState, ErrorBox, Spinner, inputClass } from '../ui/controls';
|
||||
import { Badge, BudgetBar, Card, EmptyState, ErrorBox, Segmented, Spinner, inputClass } from '../ui/controls';
|
||||
import { Icon } from '../ui/Icon';
|
||||
import { BILLABLE, CountUp, Legend, NON_BILLABLE, StackedBars } from '../ui/charts';
|
||||
import { ReportTable } from './ReportTable';
|
||||
@@ -17,17 +17,37 @@ export const CustomerReport: React.FC<{ id: number | null }> = ({ id }) => {
|
||||
const { navigate } = useRouter();
|
||||
const customers = useLoader(() => customersApi.list(), []);
|
||||
const report = useLoader(() => (id ? reportsApi.customer(id) : Promise.resolve(null)), [id]);
|
||||
const [status, setStatus] = useState<'active' | 'inactive' | 'all'>('active');
|
||||
|
||||
// Der gewaehlte Kunde bleibt in der Liste, auch wenn der Filter ihn sonst
|
||||
// ausblenden wuerde - sonst springt die Auswahl beim Umschalten weg.
|
||||
const auswahl = (customers.data ?? []).filter(
|
||||
(c) => c.id === id || status === 'all' || (status === 'inactive' ? !c.is_active : c.is_active)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<select className={`${inputClass} !w-auto min-w-[260px] max-w-full`} value={id ?? ''} onChange={(e) => navigate(e.target.value ? `reports/kunde/${e.target.value}` : 'reports/kunde')}>
|
||||
<option value="">Kunde wählen…</option>
|
||||
{(customers.data ?? []).map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<select className={`${inputClass} !w-auto min-w-[260px] max-w-full`} value={id ?? ''} onChange={(e) => navigate(e.target.value ? `reports/kunde/${e.target.value}` : 'reports/kunde')}>
|
||||
<option value="">Kunde wählen…</option>
|
||||
{auswahl.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
{c.is_active ? '' : ' (inaktiv)'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Segmented
|
||||
size="sm"
|
||||
value={status}
|
||||
onChange={setStatus}
|
||||
options={[
|
||||
{ value: 'active', label: 'Aktiv' },
|
||||
{ value: 'inactive', label: 'Inaktiv', title: 'Kunden ohne laufende Zusammenarbeit' },
|
||||
{ value: 'all', label: 'Alle' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
{!id ? (
|
||||
<EmptyState icon="building" title="Kunde auswählen" text="Alle Projekte eines Kunden mit Budget, Umsatz, Verlauf und Team." />
|
||||
) : report.error ? (
|
||||
|
||||
@@ -147,11 +147,23 @@ const TimeReport: React.FC = () => {
|
||||
)}
|
||||
<select className={`${inputClass} !w-auto max-w-[220px]`} value={customerId} onChange={(e) => setCustomerId(e.target.value ? Number(e.target.value) : '')}>
|
||||
<option value="">Alle Kunden</option>
|
||||
{customers.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
{/* Stillgelegte Kunden stehen weiter zur Wahl - die Historie bleibt ja. */}
|
||||
<optgroup label="Aktiv">
|
||||
{customers.filter((c) => c.is_active).map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
{customers.some((c) => !c.is_active) && (
|
||||
<optgroup label="Inaktiv">
|
||||
{customers.filter((c) => !c.is_active).map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
</select>
|
||||
<select className={`${inputClass} !w-auto`} value={period} onChange={(e) => setPeriod(e.target.value as Period)}>
|
||||
<option value="week">Woche</option>
|
||||
|
||||
Reference in New Issue
Block a user