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