From 3129f407f53395272b06dd551f08453c1ed051f8 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Mon, 20 Jul 2026 03:12:15 +0200 Subject: [PATCH] UI: ContactFolderTree als echter Baum - keine Trennung Firmen/Privat, ein Baum mit allen Kontakten, Personen-Symbol statt Ordner-Symbol --- .../__tests__/contacts/ContactsList.test.tsx | 3 +- .../components/contacts/ContactFolderTree.tsx | 215 ++++++++++-------- 2 files changed, 116 insertions(+), 102 deletions(-) diff --git a/frontend/src/__tests__/contacts/ContactsList.test.tsx b/frontend/src/__tests__/contacts/ContactsList.test.tsx index bdc9b17..a681c53 100644 --- a/frontend/src/__tests__/contacts/ContactsList.test.tsx +++ b/frontend/src/__tests__/contacts/ContactsList.test.tsx @@ -121,8 +121,7 @@ describe('ContactsListPage (Unified)', () => { const folderTree = getFirstByTestId('contact-folder-tree'); expect(folderTree).toBeInTheDocument(); expect(within(folderTree).getByText('Alle Kontakte')).toBeInTheDocument(); - expect(within(folderTree).getByText('Firmen')).toBeInTheDocument(); - expect(within(folderTree).getByText('Privatpersonen')).toBeInTheDocument(); + expect(within(folderTree).getByText('Ordner')).toBeInTheDocument(); }); it('renders contact list with contacts', () => { diff --git a/frontend/src/components/contacts/ContactFolderTree.tsx b/frontend/src/components/contacts/ContactFolderTree.tsx index 2a5c068..f7e0d4b 100644 --- a/frontend/src/components/contacts/ContactFolderTree.tsx +++ b/frontend/src/components/contacts/ContactFolderTree.tsx @@ -20,7 +20,7 @@ export interface ContactFolderTreeProps { contacts?: { id: string; displayname: string; type: string; folder_id?: string | null }[]; } -// ── Icons ── +// Icons const icon = (path: string, cls = 'w-4 h-4') => (
{ + e.dataTransfer.setData('text/plain', contact.id); + e.dataTransfer.effectAllowed = 'move'; + }} + onClick={() => contact.folder_id && onSelectFolder(contact.folder_id)} + className={clsx( + 'group flex items-center gap-2 px-3 py-1 text-sm cursor-grab rounded-md hover:bg-secondary-100 min-h-touch', + )} + style={{ paddingLeft: depth * 12 + 32 }} + > + {icon(ICONS.person, 'w-3.5 h-3.5 text-secondary-400 flex-shrink-0')} + {contact.displayname} +
+ ); +} + +// Folder Tree Item function FolderTreeItem({ node, @@ -143,7 +169,6 @@ function FolderTreeItem({ return (
- {/* Folder header — drop target */}
onDragOver(e, node.id)} onDragLeave={() => onDragLeave(node.id)} @@ -165,8 +190,7 @@ function FolderTreeItem({ }} > {chevron(expanded)} - {/* Contact person icon instead of folder icon */} - {icon(ICONS.contactPerson, 'w-4 h-4 text-secondary-400')} + {icon(ICONS.person, 'w-4 h-4 text-secondary-400')} {node.name} {node.contact_count > 0 && ( {node.contact_count} @@ -181,40 +205,25 @@ function FolderTreeItem({
- {/* Contacts inside folder (leaf nodes) */} {expanded && folderContacts.length > 0 && (
{folderContacts.map((contact) => ( -
{ - e.dataTransfer.setData('text/plain', contact.id); - e.dataTransfer.effectAllowed = 'move'; - }} - onClick={() => onSelectFolder(node.id)} - className={clsx( - 'group flex items-center gap-2 px-3 py-1 text-sm cursor-grab rounded-md hover:bg-secondary-100 min-h-touch', - )} - style={{ paddingLeft: depth * 12 + 32 }} - > - {icon( - contact.type === 'company' ? ICONS.company : ICONS.person, - 'w-3.5 h-3.5 text-secondary-400 flex-shrink-0', - )} - {contact.displayname} -
+ contact={contact} + depth={depth + 1} + onSelectFolder={onSelectFolder} + /> ))}
)} - {/* Child folders */} {expanded && node.children.map((child) => ( (null); const [dragOverFolderId, setDragOverFolderId] = useState(null); - const [dragContactId, setDragContactId] = useState(null); const { data: folders, isLoading: foldersLoading } = useContactFolders(); const createFolderMut = useCreateContactFolder(); @@ -258,6 +267,8 @@ export function ContactFolderTree({ const folderList = folders ?? []; const tree = buildFolderTree(folderList); + const rootContacts = contacts.filter((c) => !c.folder_id); + const itemCls = (active: boolean) => clsx( 'flex items-center gap-2 w-full px-2 py-1.5 rounded-md text-sm font-medium min-h-touch text-left', @@ -291,7 +302,7 @@ export function ContactFolderTree({ }; const handleDelete = (id: string) => { - if (!confirm('Ordner löschen? Kontakte bleiben erhalten, werden aber keinem Ordner mehr zugeordnet.')) return; + if (!confirm('Ordner l\u00f6schen? Kontakte bleiben erhalten, werden aber keinem Ordner mehr zugeordnet.')) return; deleteFolderMut.mutate(id); }; @@ -314,7 +325,6 @@ export function ContactFolderTree({ moveContactMut.mutate({ contactId, folderId }); }; - // Root drop zone (unassign from folder) const handleRootDrop = (e: React.DragEvent) => { e.preventDefault(); const contactId = e.dataTransfer.getData('text/plain'); @@ -326,81 +336,86 @@ export function ContactFolderTree({ e.preventDefault(); }; + const handleSelectFolder = useCallback((folderId: string) => { + onSelect(`folder:${folderId}` as ContactFilter); + }, [onSelect]); + return (