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 { navigate } = useRouter();
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
const [search, setSearch] = useState('');
|
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 [form, setForm] = useState<{ customer: Customer | null } | null>(null);
|
||||||
const [remove, setRemove] = useState<CustomerOverview | null>(null);
|
const [remove, setRemove] = useState<CustomerOverview | null>(null);
|
||||||
const [expanded, setExpanded] = useState<number | null>(null);
|
const [expanded, setExpanded] = useState<number | null>(null);
|
||||||
@@ -26,7 +26,7 @@ export const Customers: React.FC = () => {
|
|||||||
const customers = useMemo(() => {
|
const customers = useMemo(() => {
|
||||||
const q = search.trim().toLowerCase();
|
const q = search.trim().toLowerCase();
|
||||||
return (data?.customers ?? [])
|
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'));
|
.sort((a, b) => b.earned + b.open - (a.earned + a.open) || a.name.localeCompare(b.name, 'de'));
|
||||||
}, [data, search, status]);
|
}, [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" />
|
<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)} />
|
<input className={`${inputClass} pl-9`} placeholder="Kunde suchen…" value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||||
</div>
|
</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
|
<Segmented
|
||||||
size="sm"
|
size="sm"
|
||||||
value={layout}
|
value={layout}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
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 { Icon } from '../ui/Icon';
|
||||||
import { BILLABLE, CountUp, Legend, NON_BILLABLE, StackedBars } from '../ui/charts';
|
import { BILLABLE, CountUp, Legend, NON_BILLABLE, StackedBars } from '../ui/charts';
|
||||||
import { ReportTable } from './ReportTable';
|
import { ReportTable } from './ReportTable';
|
||||||
@@ -17,17 +17,37 @@ export const CustomerReport: React.FC<{ id: number | null }> = ({ id }) => {
|
|||||||
const { navigate } = useRouter();
|
const { navigate } = useRouter();
|
||||||
const customers = useLoader(() => customersApi.list(), []);
|
const customers = useLoader(() => customersApi.list(), []);
|
||||||
const report = useLoader(() => (id ? reportsApi.customer(id) : Promise.resolve(null)), [id]);
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<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')}>
|
<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>
|
<option value="">Kunde wählen…</option>
|
||||||
{(customers.data ?? []).map((c) => (
|
{auswahl.map((c) => (
|
||||||
<option key={c.id} value={c.id}>
|
<option key={c.id} value={c.id}>
|
||||||
{c.name}
|
{c.name}
|
||||||
|
{c.is_active ? '' : ' (inaktiv)'}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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 ? (
|
{!id ? (
|
||||||
<EmptyState icon="building" title="Kunde auswählen" text="Alle Projekte eines Kunden mit Budget, Umsatz, Verlauf und Team." />
|
<EmptyState icon="building" title="Kunde auswählen" text="Alle Projekte eines Kunden mit Budget, Umsatz, Verlauf und Team." />
|
||||||
) : report.error ? (
|
) : 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) : '')}>
|
<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>
|
<option value="">Alle Kunden</option>
|
||||||
{customers.map((c) => (
|
{/* 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}>
|
<option key={c.id} value={c.id}>
|
||||||
{c.name}
|
{c.name}
|
||||||
</option>
|
</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>
|
||||||
<select className={`${inputClass} !w-auto`} value={period} onChange={(e) => setPeriod(e.target.value as Period)}>
|
<select className={`${inputClass} !w-auto`} value={period} onChange={(e) => setPeriod(e.target.value as Period)}>
|
||||||
<option value="week">Woche</option>
|
<option value="week">Woche</option>
|
||||||
|
|||||||
Reference in New Issue
Block a user