feat: Kontakt-Ordner mit Drag&Drop Baum (wie KI-Chat Sidebar)

- Backend: ContactFolder model (hierarchisch, parent_id, sort_order)
- Migration 0022: contact_folders Tabelle + folder_id FK auf contacts
- CRUD Routes: /api/v1/contact-folders (list, create, update, delete, reorder)
- Move Contact API: /api/v1/contact-folders/contacts/{id}/move
- folder_id Filter in contacts list API
- Frontend: ContactFolderTree mit Baum-Struktur, Drag&Drop, Kontext-Menü
- Kontakt-Personen-Icon statt Ordner-Symbol
- ContactList Items draggable (List/Table/Cards View)
- React Query Hooks für Folder CRUD + Move Contact
- Alle 266 Frontend-Tests passing
This commit is contained in:
Agent Zero
2026-07-20 01:33:44 +02:00
parent 4bc11efc25
commit 29202325a6
17 changed files with 942 additions and 8 deletions
+56 -1
View File
@@ -669,6 +669,7 @@ export interface UnifiedContact {
tags?: string | null;
image?: string | null;
custom?: Record<string, any> | null;
folder_id?: string | null;
default_person_id?: string | null;
admin_contactperson_id?: string | null;
contact_persons?: ContactPerson[];
@@ -683,14 +684,16 @@ export function useUnifiedContacts(
contactType?: string,
sortBy?: string,
sortOrder?: string,
folderId?: string,
) {
const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
if (search) params.set('search', search);
if (contactType) params.set('type', contactType);
if (sortBy) params.set('sort_by', sortBy);
if (sortOrder) params.set('sort_order', sortOrder);
if (folderId) params.set('folder_id', folderId);
return useQuery({
queryKey: ['unifiedContacts', page, pageSize, search, contactType, sortBy, sortOrder],
queryKey: ['unifiedContacts', page, pageSize, search, contactType, sortBy, sortOrder, folderId],
queryFn: () =>
apiGet<PaginatedResponse<UnifiedContact>>(`/contacts?${params.toString()}`),
});
@@ -1240,3 +1243,55 @@ export function useUserGroups(userId: string | null) {
enabled: !!userId,
});
}
// ── Contact Folders ──
export function useContactFolders() {
return useQuery({
queryKey: ['contactFolders'],
queryFn: () => apiGet<import('./contactFolders').ContactFolder[]>('/contact-folders'),
});
}
export function useCreateContactFolder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: { name: string; parent_id?: string }) => apiPost('/contact-folders', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contactFolders'] });
},
});
}
export function useUpdateContactFolder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: { name?: string; parent_id?: string | null; sort_order?: number } }) =>
apiPut(`/contact-folders/${id}`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contactFolders'] });
},
});
}
export function useDeleteContactFolder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiDelete(`/contact-folders/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contactFolders'] });
},
});
}
export function useMoveContactToFolder() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ contactId, folderId }: { contactId: string; folderId: string | null }) =>
apiPut(`/contact-folders/contacts/${contactId}/move`, { folder_id: folderId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
queryClient.invalidateQueries({ queryKey: ['contactFolders'] });
},
});
}