Scanner: Trefferliste per Klick auf die Spaltenüberschrift sortieren
- sort + dir serverseitig für Quote, Agent, Stelle, Unternehmen, Börse, Veröffentlicht, Status, Gefunden; leere Werte immer unten - neue Spalte Veröffentlicht (Alter der Anzeige, Datum im Tooltip) - Feld Sortierung bleibt als Schnellwahl Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1ad774fdd2
commit
0aa8f17b28
@@ -64,7 +64,7 @@ export const ScannerDashboard: React.FC = () => {
|
||||
const [knownOnly, setKnownOnly] = useState(false);
|
||||
const [showDiscarded, setShowDiscarded] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [sort, setSort] = useState<'known' | 'score' | 'published' | 'oldest'>('known');
|
||||
const [sort, setSort] = useState<SortState>({ key: 'known', dir: 'desc' });
|
||||
/** Alter der Anzeige: alle | bis 21 Tage | älter als 21 Tage */
|
||||
const [age, setAge] = useState<'' | 'recent' | 'old'>('');
|
||||
const [alarmOnly, setAlarmOnly] = useState(false);
|
||||
@@ -83,7 +83,8 @@ export const ScannerDashboard: React.FC = () => {
|
||||
q: search.trim() || undefined,
|
||||
age: age || undefined,
|
||||
alarm: alarmOnly ? 1 : undefined,
|
||||
sort,
|
||||
sort: sort.key,
|
||||
dir: sort.dir,
|
||||
});
|
||||
setMatches(data.matches);
|
||||
setTotal(data.total);
|
||||
@@ -290,11 +291,20 @@ export const ScannerDashboard: React.FC = () => {
|
||||
/>
|
||||
</Filter>
|
||||
<Filter label="Sortierung" className="w-48">
|
||||
<select value={sort} onChange={(event) => setSort(event.target.value as typeof sort)} className={inputClass}>
|
||||
<option value="known">bekannte Firmen zuerst</option>
|
||||
<option value="score">beste Quote</option>
|
||||
<option value="published">neueste Anzeige</option>
|
||||
<option value="oldest">älteste Anzeige</option>
|
||||
<select
|
||||
value={presetOf(sort)}
|
||||
onChange={(event) => {
|
||||
const preset = PRESETS[event.target.value];
|
||||
if (preset) setSort(preset.sort);
|
||||
}}
|
||||
className={inputClass}
|
||||
>
|
||||
{Object.entries(PRESETS).map(([value, preset]) => (
|
||||
<option key={value} value={value}>
|
||||
{preset.label}
|
||||
</option>
|
||||
))}
|
||||
{presetOf(sort) === '' && <option value="">nach Spalte</option>}
|
||||
</select>
|
||||
</Filter>
|
||||
<Filter label="Suche" className="w-44">
|
||||
@@ -339,13 +349,14 @@ export const ScannerDashboard: React.FC = () => {
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-ink-200 text-left text-[11px] font-semibold uppercase tracking-wider text-ink-500">
|
||||
<th className="px-4 py-3">Match</th>
|
||||
<th className="px-4 py-3">Agent</th>
|
||||
<th className="px-4 py-3">Stelle</th>
|
||||
<th className="px-4 py-3">Unternehmen</th>
|
||||
<th className="px-4 py-3">Börse</th>
|
||||
<th className="px-4 py-3">Status</th>
|
||||
<th className="px-4 py-3">Gefunden am</th>
|
||||
<SortHeader label="Match" column="score" sort={sort} onSort={setSort} />
|
||||
<SortHeader label="Agent" column="agent" sort={sort} onSort={setSort} />
|
||||
<SortHeader label="Stelle" column="title" sort={sort} onSort={setSort} />
|
||||
<SortHeader label="Unternehmen" column="company" sort={sort} onSort={setSort} />
|
||||
<SortHeader label="Börse" column="source" sort={sort} onSort={setSort} />
|
||||
<SortHeader label="Veröffentlicht" column="published" sort={sort} onSort={setSort} />
|
||||
<SortHeader label="Status" column="status" sort={sort} onSort={setSort} />
|
||||
<SortHeader label="Gefunden am" column="created" sort={sort} onSort={setSort} />
|
||||
<th className="px-4 py-3 text-right">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -374,7 +385,6 @@ export const ScannerDashboard: React.FC = () => {
|
||||
</button>
|
||||
<p className="mt-0.5 text-xs text-ink-500">
|
||||
{[match.location, match.country, match.employment_type].filter(Boolean).join(' · ') || '–'}
|
||||
<span className="text-ink-400"> · {formatAge(match.published_at)}</span>
|
||||
</p>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
@@ -395,6 +405,12 @@ export const ScannerDashboard: React.FC = () => {
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-ink-600">{SOURCE_LABELS[match.source] ?? match.source}</td>
|
||||
<td
|
||||
className="whitespace-nowrap px-4 py-3 text-ink-500"
|
||||
title={match.published_at ? formatDateTime(match.published_at) : 'Die Börse nennt kein Datum'}
|
||||
>
|
||||
{formatAge(match.published_at)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<StatusBadge status={match.status} />
|
||||
</td>
|
||||
@@ -443,6 +459,55 @@ const Kpi: React.FC<{ value: React.ReactNode; label: string; icon: string }> = (
|
||||
</div>
|
||||
);
|
||||
|
||||
/* ---------------------------------------------------------- Sortierung */
|
||||
|
||||
type SortKey = 'known' | 'score' | 'agent' | 'title' | 'company' | 'source' | 'published' | 'status' | 'created';
|
||||
type SortState = { key: SortKey; dir: 'asc' | 'desc' };
|
||||
|
||||
/** Schnellwahl im Feld "Sortierung" - dieselben Werte wie ein Spaltenklick. */
|
||||
const PRESETS: Record<string, { label: string; sort: SortState }> = {
|
||||
known: { label: 'bekannte Firmen zuerst', sort: { key: 'known', dir: 'desc' } },
|
||||
score: { label: 'beste Quote', sort: { key: 'score', dir: 'desc' } },
|
||||
published: { label: 'neueste Anzeige', sort: { key: 'published', dir: 'desc' } },
|
||||
oldest: { label: 'älteste Anzeige', sort: { key: 'published', dir: 'asc' } },
|
||||
};
|
||||
|
||||
const presetOf = (sort: SortState) =>
|
||||
Object.entries(PRESETS).find(([, preset]) => preset.sort.key === sort.key && preset.sort.dir === sort.dir)?.[0] ?? '';
|
||||
|
||||
/** Texte beginnen beim ersten Klick mit A-Z, Zahlen und Daten mit dem größten/neuesten Wert. */
|
||||
const TEXT_COLUMNS: SortKey[] = ['agent', 'title', 'company', 'source', 'status'];
|
||||
|
||||
const SortHeader: React.FC<{
|
||||
label: string;
|
||||
column: SortKey;
|
||||
sort: SortState;
|
||||
onSort: (sort: SortState) => void;
|
||||
}> = ({ label, column, sort, onSort }) => {
|
||||
const active = sort.key === column;
|
||||
const next = (): SortState =>
|
||||
active
|
||||
? { key: column, dir: sort.dir === 'asc' ? 'desc' : 'asc' }
|
||||
: { key: column, dir: TEXT_COLUMNS.includes(column) ? 'asc' : 'desc' };
|
||||
return (
|
||||
<th className="px-4 py-3" aria-sort={active ? (sort.dir === 'asc' ? 'ascending' : 'descending') : 'none'}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSort(next())}
|
||||
title="Nach dieser Spalte sortieren"
|
||||
className={`inline-flex items-center gap-1 uppercase tracking-wider hover:text-ink-900 ${
|
||||
active ? 'text-brand-700' : ''
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
<span className={`text-[10px] ${active ? '' : 'opacity-0'}`} aria-hidden="true">
|
||||
{active && sort.dir === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
</button>
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
const Filter: React.FC<{ label: string; className?: string; children: React.ReactNode }> = ({ label, className = '', children }) => (
|
||||
<label className={`flex shrink-0 flex-col gap-1 ${className}`}>
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-ink-500">{label}</span>
|
||||
|
||||
Reference in New Issue
Block a user