revert(frontend): Frontend auf letzten funktionierenden Stand 5680179 zurückgesetzt — i18n-Massen-Batch brach Dashboard-Shell in Produktion; Render-Loop-Fix und DSGVO-Antrags-UI liegen sicher in Historie für kontrollierten Wiedereinspiel

This commit is contained in:
Agent Zero
2026-08-27 09:14:21 +02:00
parent bea479bfad
commit 66c11d3d64
119 changed files with 925 additions and 1477 deletions
@@ -237,7 +237,7 @@ export function ContactEditForm({ onClose, contact, onSaved }: ContactEditFormPr
error={errors.name?.message}
required
data-testid="contact-name-input"
placeholder={t('contactEditForm.techcorpgmbh')}
placeholder="TechCorp GmbH"
/>
) : (
<div className="grid grid-cols-2 gap-3">
@@ -257,7 +257,7 @@ export function ContactEditForm({ onClose, contact, onSaved }: ContactEditFormPr
)}
{/* Code */}
<Input label={t('contacts.code')} {...register('code')} placeholder={t('contactEditForm.k00123')} />
<Input label={t('contacts.code')} {...register('code')} placeholder="K-00123" />
{/* Communication */}
<div className="border border-secondary-200 rounded-lg p-3">
@@ -299,7 +299,7 @@ export function ContactEditForm({ onClose, contact, onSaved }: ContactEditFormPr
<div className="border border-secondary-200 rounded-lg p-3">
<h3 className="text-sm font-semibold text-secondary-700 mb-2">{t('contacts.notes')}</h3>
<div className="space-y-3">
<Input label={t('contacts.tags')} {...register('tags')} placeholder={t('contactEditForm.tag1tag2')} />
<Input label={t('contacts.tags')} {...register('tags')} placeholder="tag1, tag2" />
<div>
<label className="block text-sm font-medium text-secondary-700 mb-1">{t('contacts.projectnote')}</label>
<textarea
@@ -162,7 +162,6 @@ function FolderTreeItem({
multiSelectedFolders?: string[];
onToggleMultiSelect?: (folderId: string) => void;
}) {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(true);
const folderKey = `folder:${node.id}` as ContactFilter;
const isActive = selectedFilter === folderKey;
@@ -229,8 +228,8 @@ function FolderTreeItem({
type="button"
onClick={(e) => { e.stopPropagation(); e.preventDefault(); onMoreClick(e, node.id); }}
className="flex-shrink-0 text-secondary-400 hover:text-primary-600 p-1.5 rounded hover:bg-secondary-100 transition-colors touch-manipulation"
title={t('contactFolderTree.optionen')}
aria-label={t('contactFolderTree.optionen')}
title="Optionen"
aria-label="Optionen"
>
{icon(ICONS.more, 'w-4 h-4')}
</button>
@@ -447,8 +446,8 @@ export function ContactFolderTree({
<button
onClick={handleToggleMultiSelectMode}
className={`text-secondary-400 hover:text-primary-600 p-0.5 ${multiSelectMode ? 'text-primary-600 bg-primary-50 rounded' : ''}`}
title={t('contactFolderTree.mehrereordnerauswählen')}
aria-label={t('contactFolderTree.mehrereordnerauswählen')}
title="Mehrere Ordner auswählen"
aria-label="Mehrere Ordner auswählen"
>
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<rect x="3" y="3" width="7" height="7" rx="1" />
@@ -462,8 +461,8 @@ export function ContactFolderTree({
<button
onClick={handleNewFolder}
className="text-secondary-400 hover:text-primary-600 p-0.5"
title={t('contactFolderTree.neuerordner')}
aria-label={t('contactFolderTree.neuerordner')}
title="Neuer Ordner"
aria-label="Neuer Ordner"
>
{icon(ICONS.plus, 'w-3.5 h-3.5')}
</button>
@@ -513,7 +512,7 @@ export function ContactFolderTree({
)}
{tree.length === 0 && !foldersLoading && !loading && (
<div className="px-2 py-1 text-xs text-secondary-400">{t('contactFolderTree.keineordnervorhanden')}</div>
<div className="px-2 py-1 text-xs text-secondary-400">Keine Ordner vorhanden</div>
)}
</div>
@@ -575,7 +574,7 @@ export function ContactFolderTree({
<>
<div className="fixed inset-0 z-[9998]" onClick={() => setColorPicker(null)} />
<div className="fixed z-[9999] bg-white border border-secondary-200 rounded-lg shadow-lg p-3 min-w-[200px]" style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}>
<div className="text-sm font-semibold text-secondary-700 mb-2">{t('contactFolderTree.farbewählen')}</div>
<div className="text-sm font-semibold text-secondary-700 mb-2">Farbe wählen</div>
<div className="grid grid-cols-6 gap-1.5 mb-3">
{['#ef4444', '#f97316', '#f59e0b', '#eab308', '#84cc16', '#22c55e', '#10b981', '#14b8a6', '#06b6d4', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#ec4899', '#f43f5e', '#64748b', '#475569'].map((c) => (
<button
@@ -969,7 +969,7 @@ export function ContactList({
onClick={() => setBulkFolderOpen(!bulkFolderOpen)}
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-white/20 hover:bg-white/30 rounded transition-colors"
>
{t('contactList.ordnerzuweisen')}
Ordner zuweisen
</button>
{bulkFolderOpen && (
<div className="absolute right-0 top-full mt-1 bg-white text-secondary-800 rounded-lg shadow-lg border border-secondary-200 max-h-60 overflow-y-auto min-w-[200px] z-30">
@@ -987,7 +987,7 @@ export function ContactList({
</button>
))}
{folderList.length === 0 && (
<div className="px-3 py-2 text-xs text-secondary-400">{t('contactList.keineordner')}</div>
<div className="px-3 py-2 text-xs text-secondary-400">Keine Ordner</div>
)}
</div>
)}
@@ -999,7 +999,7 @@ export function ContactList({
onClick={() => setBulkTagsOpen(!bulkTagsOpen)}
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-white/20 hover:bg-white/30 rounded transition-colors"
>
{t('contactList.tagshinzufügen')}
Tags hinzufügen
</button>
{bulkTagsOpen && (
<div className="absolute right-0 top-full mt-1 bg-white text-secondary-800 rounded-lg shadow-lg border border-secondary-200 p-2 z-30">
@@ -1007,7 +1007,7 @@ export function ContactList({
type="text"
value={bulkTagsInput}
onChange={(e) => setBulkTagsInput(e.target.value)}
placeholder={t('contactList.tag1tag2')}
placeholder="tag1, tag2, ..."
className="w-48 px-2 py-1 text-xs border border-secondary-200 rounded focus:outline-none focus:border-primary-400"
/>
<button
@@ -1032,7 +1032,7 @@ export function ContactList({
onClick={clearSelection}
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium bg-white/20 hover:bg-white/30 rounded transition-colors"
>
{t('contactList.auswahlaufheben')}
Auswahl aufheben
</button>
</div>
)}
@@ -1041,9 +1041,9 @@ export function ContactList({
{bulkDeleteConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-lg shadow-xl p-6 max-w-sm">
<h3 className="text-lg font-semibold text-secondary-900 mb-2">{t('contactList.löschenbestätigen')}</h3>
<h3 className="text-lg font-semibold text-secondary-900 mb-2">Löschen bestätigen</h3>
<p className="text-sm text-secondary-600 mb-4">
{selectedContactIds?.size || 0} {t('contactList.kontaktewirklichlöschen')}
{selectedContactIds?.size || 0} Kontakt(e) wirklich löschen?
</p>
<div className="flex justify-end gap-2">
<button
@@ -1073,7 +1073,7 @@ export function ContactList({
{isCustomSortActive && customOrder.length > 0 && (
<div className="flex items-center gap-1.5 px-3 py-1 bg-amber-50 border-b border-amber-200 text-xs text-amber-700">
<Info className="w-3 h-3" />
<span>{t('contactList.customsortierungaktivdraganddrop')}</span>
<span>Custom Sortierung aktiv Drag-and-Drop zum Umsortieren</span>
</div>
)}
@@ -1138,15 +1138,15 @@ export function ContactList({
<div ref={colMenuRef} className="relative">
<button
onClick={() => setColMenuOpen(!colMenuOpen)}
title={t('contactList.spaltenverwalten')}
aria-label={t('contactList.spaltenverwalten')}
title="Spalten verwalten"
aria-label="Spalten verwalten"
className="p-1 rounded hover:bg-secondary-100 text-secondary-500 hover:text-secondary-700 transition-colors"
>
<Settings className="w-3.5 h-3.5" />
</button>
{colMenuOpen && (
<div className="absolute right-0 top-full mt-1 bg-white rounded-lg shadow-lg border border-secondary-200 max-h-80 overflow-y-auto min-w-[200px] z-30">
<div className="px-3 py-2 text-xs font-semibold text-secondary-700 border-b border-secondary-100">{t('contactList.spaltenverwalten')}</div>
<div className="px-3 py-2 text-xs font-semibold text-secondary-700 border-b border-secondary-100">Spalten verwalten</div>
{ALL_COLUMNS.map((col) => (
<label
key={col.key}
@@ -8,7 +8,6 @@ import React, { useState, useRef, useEffect, useMemo } from 'react';
import { Filter, Plus, X, ChevronDown, Bookmark } from 'lucide-react';
import type { UnifiedContact } from '@/api/unifiedContacts';
import { useCustomFieldDefinitions, type CustomFieldDefinition } from '@/api/customFieldDefinitions';
import { useTranslation } from 'react-i18next';
// ─── Field definitions ───────────────────────────────────────────────────────
@@ -246,7 +245,6 @@ function newConditionId() {
}
export function FilterPanel({ filters, onFiltersChange, contactType, savedFilters = [], onSaveFilter, onLoadFilter, onDeleteFilter }: FilterPanelProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -375,8 +373,8 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
<div ref={btnRef} className="relative flex-shrink-0">
<button
onClick={handleToggle}
title={t('filterPanel.filter')}
aria-label={t('filterPanel.filter')}
title="Filter"
aria-label="Filter"
className={`
inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium
transition-colors duration-100 cursor-pointer relative
@@ -417,7 +415,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
onClick={clearAll}
className="text-xs text-secondary-500 hover:text-red-600 transition-colors"
>
{t('filterPanel.allelöschen')}
Alle löschen
</button>
)}
<button
@@ -432,7 +430,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
{/* Logic toggle */}
{activeCount > 0 && (
<div className="flex items-center gap-2 px-4 py-2 border-b border-secondary-100">
<span className="text-xs text-secondary-500">{t('filterPanel.bedingungenverknüpfen')}</span>
<span className="text-xs text-secondary-500">Bedingungen verknüpfen:</span>
<button
onClick={toggleLogic}
className={`
@@ -485,7 +483,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
{/* Saved filters */}
{savedFilters.length > 0 && (
<div className="px-4 py-2 border-b border-secondary-100">
<div className="text-[10px] font-semibold uppercase tracking-wide text-secondary-400 mb-1.5">{t('filterPanel.gespeichertefilter')}</div>
<div className="text-[10px] font-semibold uppercase tracking-wide text-secondary-400 mb-1.5">Gespeicherte Filter</div>
<div className="space-y-0.5">
{savedFilters.map((sf) => (
<div key={sf.id} className="flex items-center gap-1 group">
@@ -512,7 +510,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
<div className="px-4 py-3 space-y-2">
{filters.conditions.length === 0 && (
<div className="text-center py-6 text-xs text-secondary-400">
{t('filterPanel.keinefilteraktivklickeuntenum')}
Keine Filter aktiv. Klicke unten um eine Bedingung hinzuzufügen.
</div>
)}
{filters.conditions.map((cond, idx) => {
@@ -572,7 +570,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
onChange={(e) => updateCondition(cond.id, { value: e.target.value })}
className="px-2 py-1.5 text-xs border border-secondary-200 rounded bg-white cursor-pointer focus:outline-none focus:border-primary-400"
>
<option value="">{t('filterPanel.wählen')}</option>
<option value=""> wählen </option>
{def.options?.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
@@ -589,7 +587,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
type="text"
value={cond.value}
onChange={(e) => updateCondition(cond.id, { value: e.target.value })}
placeholder={t('filterPanel.wert')}
placeholder="Wert…"
className="w-24 px-2 py-1.5 text-xs border border-secondary-200 rounded focus:outline-none focus:border-primary-400"
/>
)
@@ -617,7 +615,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
>
<Plus className="w-3.5 h-3.5" />
{t('filterPanel.bedingunghinzufügen')}
Bedingung hinzufügen
</button>
{activeCount > 0 && onSaveFilter && (
<button
@@ -625,7 +623,7 @@ export function FilterPanel({ filters, onFiltersChange, contactType, savedFilter
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
>
<Bookmark className="w-3.5 h-3.5" />
{t('filterPanel.filterspeichern')}
Filter speichern
</button>
)}
</div>
@@ -11,7 +11,6 @@ import {
import { useUsers } from '@/api/users';
import { useGroups } from '@/api/groups';
import type { FolderPermission } from '@/api/contactFolders';
import { useTranslation } from 'react-i18next';
interface FolderPermissionDialogProps {
folderId: string;
@@ -37,7 +36,6 @@ function permLabel(level: string) {
}
export function FolderPermissionDialog({ folderId, folderName, onClose }: FolderPermissionDialogProps) {
const { t } = useTranslation();
const { data: permData, isLoading } = useFolderPermissions(folderId);
const { data: usersData } = useUsers(1, 100);
const { data: groupsData } = useGroups();
@@ -111,7 +109,7 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
<button
onClick={onClose}
className="text-secondary-400 hover:text-secondary-600 p-1 rounded-md hover:bg-secondary-100"
aria-label={t('folderPermissionDialog.schließen')}
aria-label="Schließen"
>
<X className="w-5 h-5" strokeWidth={2} />
</button>
@@ -121,9 +119,9 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
<div className="flex-1 overflow-y-auto px-5 py-4">
{/* Info banner */}
<div className="mb-4 p-3 bg-primary-50 border border-primary-200 rounded-lg text-sm text-primary-700">
<p className="font-medium mb-1">{t('folderPermissionDialog.ordnerteilen')}</p>
<p className="font-medium mb-1">Ordner teilen</p>
<p className="text-primary-600">
{t('folderPermissionDialog.gewährebenutzernodergruppenzugriffauf')}
Gewähre Benutzern oder Gruppen Zugriff auf diesen Ordner. Mit Vererben" gelten die Rechte auch für alle Unterordner.
</p>
</div>
@@ -132,7 +130,7 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
<div className="text-sm text-secondary-400 py-4 text-center">Laden…</div>
) : permissions.length === 0 ? (
<div className="text-sm text-secondary-400 py-4 text-center">
{t('folderPermissionDialog.nochkeineberechtigungenvergebendieserord')}
Noch keine Berechtigungen vergeben. Dieser Ordner ist nur für den Besitzer sichtbar.
</div>
) : (
<div className="space-y-2">
@@ -178,8 +176,8 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
<button
onClick={() => handleDelete(perm)}
className="text-secondary-400 hover:text-red-600 p-1.5 rounded-md hover:bg-red-50 flex-shrink-0"
title={t('folderPermissionDialog.entfernen')}
aria-label={t('folderPermissionDialog.berechtigungentfernen')}
title="Entfernen"
aria-label="Berechtigung entfernen"
>
<Trash2 className="w-4 h-4" strokeWidth={2} />
</button>
@@ -194,7 +192,7 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
<div className="mt-4 p-4 border-2 border-primary-200 rounded-lg bg-primary-50/50">
<div className="flex items-center gap-2 mb-3">
<Plus className="w-4 h-4 text-primary-600" strokeWidth={2} />
<span className="text-sm font-medium text-secondary-700">{t('folderPermissionDialog.neueberechtigung')}</span>
<span className="text-sm font-medium text-secondary-700">Neue Berechtigung</span>
</div>
{/* Type toggle */}
@@ -266,7 +264,7 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
onChange={(e) => setAddInherit(e.target.checked)}
className="w-4 h-4 rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
/>
{t('folderPermissionDialog.aufunterordnervererben')}
Auf Unterordner vererben
</label>
{/* Actions */}
@@ -292,7 +290,7 @@ export function FolderPermissionDialog({ folderId, folderName, onClose }: Folder
className="mt-4 w-full flex items-center justify-center gap-2 py-2.5 text-sm text-primary-600 border-2 border-dashed border-primary-200 rounded-lg hover:bg-primary-50 hover:border-primary-300 transition-colors"
>
<Plus className="w-4 h-4" strokeWidth={2} />
{t('folderPermissionDialog.berechtigunghinzufügen')}
Berechtigung hinzufügen
</button>
)}
</div>
@@ -8,7 +8,6 @@ import React, { useState, useRef, useEffect, useMemo } from 'react';
import { Group as GroupIcon, Plus, X } from 'lucide-react';
import type { UnifiedContact } from '@/api/unifiedContacts';
import { useCustomFieldDefinitions } from '@/api/customFieldDefinitions';
import { useTranslation } from 'react-i18next';
// ─── Field definitions ────────────────────────────────────────────────────────
@@ -173,7 +172,6 @@ function newGroupId() {
}
export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPanelProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -291,8 +289,8 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
<div ref={btnRef} className="relative flex-shrink-0">
<button
onClick={handleToggle}
title={t('groupPanel.gruppierung')}
aria-label={t('groupPanel.gruppierung')}
title="Gruppierung"
aria-label="Gruppierung"
className={`
inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium
transition-colors duration-100 cursor-pointer relative
@@ -333,7 +331,7 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
onClick={clearAll}
className="text-xs text-secondary-500 hover:text-red-600 transition-colors"
>
{t('groupPanel.allelöschen')}
Alle löschen
</button>
)}
<button
@@ -349,7 +347,7 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
{activeCount === 0 && (
<div className="px-4 py-3 border-b border-secondary-100">
<div className="text-center py-4 text-xs text-secondary-400">
{t('groupPanel.keinegruppierungaktivalledatensätzewerde')}
Keine Gruppierung aktiv. Alle Datensätze werden in einer flachen Liste angezeigt.
</div>
</div>
)}
@@ -437,7 +435,7 @@ export function GroupPanel({ groupState, onGroupChange, contactType }: GroupPane
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
>
<Plus className="w-3.5 h-3.5" />
{t('groupPanel.gruppierunghinzufügen')}
Gruppierung hinzufügen
</button>
<button
onClick={() => setOpen(false)}
@@ -4,7 +4,6 @@
import React, { useState } from 'react';
import { Bookmark, Check, Folder, Filter, Group as GroupIcon, ArrowDownAZ, LayoutGrid } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export interface SaveViewSelection {
folder: boolean;
@@ -33,7 +32,6 @@ const defaultSelection: SaveViewSelection = {
};
export function SaveViewDialog({ open, onClose, onSave, hasFilter, hasGroup, hasSort, hasFolder }: SaveViewDialogProps) {
const { t } = useTranslation();
const [name, setName] = useState('');
const [selection, setSelection] = useState<SaveViewSelection>(defaultSelection);
@@ -68,19 +66,19 @@ export function SaveViewDialog({ open, onClose, onSave, hasFilter, hasGroup, has
{/* Header */}
<div className="flex items-center gap-2 px-5 py-4 border-b border-secondary-100">
<Bookmark className="w-5 h-5 text-primary-600" strokeWidth={2} />
<h2 className="text-base font-semibold text-secondary-800">{t('saveViewDialog.ansichtspeichern')}</h2>
<h2 className="text-base font-semibold text-secondary-800">Ansicht speichern</h2>
</div>
{/* Body */}
<div className="px-5 py-4 space-y-4">
{/* Name input */}
<div>
<label className="block text-xs font-medium text-secondary-600 mb-1">{t('saveViewDialog.namederansicht')}</label>
<label className="block text-xs font-medium text-secondary-600 mb-1">Name der Ansicht</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t('saveViewDialog.zbmeinefirmenkontakte')}
placeholder="z.B. Meine Firmen-Kontakte"
autoFocus
onKeyDown={(e) => { if (e.key === 'Enter') handleSave(); }}
className="w-full px-3 py-2 text-sm border border-secondary-200 rounded-md focus:outline-none focus:border-primary-400 focus:ring-1 focus:ring-primary-300"
@@ -89,7 +87,7 @@ export function SaveViewDialog({ open, onClose, onSave, hasFilter, hasGroup, has
{/* Component selection */}
<div>
<label className="block text-xs font-medium text-secondary-600 mb-2">{t('saveViewDialog.wassollgespeichertwerden')}</label>
<label className="block text-xs font-medium text-secondary-600 mb-2">Was soll gespeichert werden?</label>
<div className="space-y-1.5">
{options.map((opt) => (
<label
@@ -114,7 +112,7 @@ export function SaveViewDialog({ open, onClose, onSave, hasFilter, hasGroup, has
</div>
</span>
{!opt.available && (
<span className="text-[10px] text-secondary-400 italic">{t('saveViewDialog.nichtaktiv')}</span>
<span className="text-[10px] text-secondary-400 italic">nicht aktiv</span>
)}
</label>
))}
@@ -8,7 +8,6 @@ import React, { useState, useRef, useEffect, useMemo } from 'react';
import { ArrowDownAZ, ArrowUpZA, Plus, X } from 'lucide-react';
import type { UnifiedContact } from '@/api/unifiedContacts';
import { useCustomFieldDefinitions } from '@/api/customFieldDefinitions';
import { useTranslation } from 'react-i18next';
// ─── Field definitions (reuse from FilterPanel) ────────────────────────────────
@@ -165,7 +164,6 @@ function newSortId() {
}
export function SortPanel({ sortState, onSortChange, contactType }: SortPanelProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const btnRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
@@ -284,8 +282,8 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
<div ref={btnRef} className="relative flex-shrink-0">
<button
onClick={handleToggle}
title={t('sortPanel.sortieren')}
aria-label={t('sortPanel.sortieren')}
title="Sortieren"
aria-label="Sortieren"
className={`
inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium
transition-colors duration-100 cursor-pointer relative
@@ -326,7 +324,7 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
onClick={clearAll}
className="text-xs text-secondary-500 hover:text-red-600 transition-colors"
>
{t('sortPanel.allelöschen')}
Alle löschen
</button>
)}
<button
@@ -342,7 +340,7 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
{activeCount === 0 && (
<div className="px-4 py-3 border-b border-secondary-100">
<div className="text-center py-4 text-xs text-secondary-400">
{t('sortPanel.keinesortierungaktivdatensätzekönnenper')}
Keine Sortierung aktiv. Datensätze können per Drag-and-Drop umsortiert werden.
</div>
</div>
)}
@@ -450,7 +448,7 @@ export function SortPanel({ sortState, onSortChange, contactType }: SortPanelPro
className="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 rounded transition-colors"
>
<Plus className="w-3.5 h-3.5" />
{t('sortPanel.sortierunghinzufügen')}
Sortierung hinzufügen
</button>
<button
onClick={() => setOpen(false)}