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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user