Phase 3: Saved Filters UI, Entity History UI, Activity Timeline, API Docs Link

- Saved Filters: SavedFilterBar (dropdown, apply, delete), SaveFilterDialog (name + save)
- Entity History: EntityHistoryPanel (timeline, restore, undo), HistoryDiff (field changes visual)
- Activity Timeline: ActivityTimelinePage (grouped by day, pagination), ActivityFilter (user/entity/action/date)
- API Docs Link: TopBar user menu entry, SettingsSystem Entwickler section (Swagger, ReDoc, OpenAPI)
- Routes: /activity registered
- Menu items: Aktivitäten added to automation plugin manifest
This commit is contained in:
Agent Zero
2026-07-26 03:08:26 +02:00
parent a7e3890634
commit 10dcc8ae90
10 changed files with 1263 additions and 1 deletions
@@ -0,0 +1,151 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Filter, RotateCcw } from 'lucide-react';
import { Select } from '@/components/ui/Select';
import { Button } from '@/components/ui/Button';
export interface ActivityFilterValues {
user?: string;
entity_type?: string;
action?: string;
date_from?: string;
date_to?: string;
}
export interface ActivityFilterProps {
onFilter: (filters: ActivityFilterValues) => void;
initialValues?: ActivityFilterValues;
}
const ENTITY_TYPE_OPTIONS = [
{ value: '', label: 'Alle' },
{ value: 'contact', label: 'Kontakt' },
{ value: 'mail', label: 'E-Mail' },
{ value: 'calendar', label: 'Kalender' },
{ value: 'dms', label: 'Dokument' },
{ value: 'task', label: 'Aufgabe' },
];
const ACTION_OPTIONS = [
{ value: '', label: 'Alle' },
{ value: 'create', label: 'Erstellt' },
{ value: 'update', label: 'Aktualisiert' },
{ value: 'delete', label: 'Gelöscht' },
];
export function ActivityFilter({ onFilter, initialValues }: ActivityFilterProps) {
const { t } = useTranslation();
const [user, setUser] = useState(initialValues?.user ?? '');
const [entityType, setEntityType] = useState(initialValues?.entity_type ?? '');
const [action, setAction] = useState(initialValues?.action ?? '');
const [dateFrom, setDateFrom] = useState(initialValues?.date_from ?? '');
const [dateTo, setDateTo] = useState(initialValues?.date_to ?? '');
const handleApply = (e: React.FormEvent) => {
e.preventDefault();
onFilter({
user: user.trim() || undefined,
entity_type: entityType || undefined,
action: action || undefined,
date_from: dateFrom || undefined,
date_to: dateTo || undefined,
});
};
const handleReset = () => {
setUser('');
setEntityType('');
setAction('');
setDateFrom('');
setDateTo('');
onFilter({});
};
return (
<form
onSubmit={handleApply}
className="bg-white rounded-lg shadow-sm border border-secondary-200 p-4 mb-6"
data-testid="activity-filter"
>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
<div className="w-full">
<label
htmlFor="activity-filter-user"
className="block text-sm font-medium text-secondary-700 mb-1"
>
{t('activity.filterUser', 'Benutzer')}
</label>
<input
id="activity-filter-user"
type="text"
value={user}
onChange={(e) => setUser(e.target.value)}
placeholder="Benutzername"
className="block w-full rounded-md border border-secondary-300 px-3 py-2 text-base min-h-touch text-secondary-900 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/>
</div>
<Select
label={t('activity.filterEntityType', 'Entitätstyp')}
options={ENTITY_TYPE_OPTIONS}
value={entityType}
onChange={(e) => setEntityType(e.target.value)}
/>
<Select
label={t('activity.filterAction', 'Aktion')}
options={ACTION_OPTIONS}
value={action}
onChange={(e) => setAction(e.target.value)}
/>
<div className="w-full">
<label
htmlFor="activity-filter-date-from"
className="block text-sm font-medium text-secondary-700 mb-1"
>
{t('activity.filterDateFrom', 'Von Datum')}
</label>
<input
id="activity-filter-date-from"
type="date"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
className="block w-full rounded-md border border-secondary-300 px-3 py-2 text-base min-h-touch text-secondary-900 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/>
</div>
<div className="w-full">
<label
htmlFor="activity-filter-date-to"
className="block text-sm font-medium text-secondary-700 mb-1"
>
{t('activity.filterDateTo', 'Bis Datum')}
</label>
<input
id="activity-filter-date-to"
type="date"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
className="block w-full rounded-md border border-secondary-300 px-3 py-2 text-base min-h-touch text-secondary-900 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/>
</div>
</div>
<div className="flex items-center gap-3 mt-4">
<Button type="submit" variant="primary" size="md" icon={<Filter className="h-4 w-4" />}>
{t('activity.applyFilter', 'Filter anwenden')}
</Button>
<Button
type="button"
variant="secondary"
size="md"
icon={<RotateCcw className="h-4 w-4" />}
onClick={handleReset}
>
{t('activity.resetFilter', 'Zurücksetzen')}
</Button>
</div>
</form>
);
}