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:
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Contact folder API client — CRUD, move contacts, reorder.
|
||||
*/
|
||||
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from './client';
|
||||
|
||||
export interface ContactFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
parent_id: string | null;
|
||||
user_id: string;
|
||||
sort_order: number;
|
||||
contact_count: number;
|
||||
}
|
||||
|
||||
export interface ContactFolderTreeNode extends ContactFolder {
|
||||
children: ContactFolderTreeNode[];
|
||||
}
|
||||
|
||||
// ── Folders ──
|
||||
|
||||
export const fetchContactFolders = () =>
|
||||
apiGet<ContactFolder[]>('/contact-folders');
|
||||
|
||||
export const createContactFolder = (data: { name: string; parent_id?: string }) =>
|
||||
apiPost<ContactFolder>('/contact-folders', data);
|
||||
|
||||
export const updateContactFolder = (id: string, data: Partial<ContactFolder>) =>
|
||||
apiPut<ContactFolder>(`/contact-folders/${id}`, data);
|
||||
|
||||
export const deleteContactFolder = (id: string) =>
|
||||
apiDelete(`/contact-folders/${id}`);
|
||||
|
||||
export const reorderContactFolders = (folderId: string, orders: { id: string; sort_order: number; parent_id?: string | null }[]) =>
|
||||
apiPut(`/contact-folders/${folderId}/reorder`, orders);
|
||||
|
||||
// ── Move contact ──
|
||||
|
||||
export const moveContactToFolder = (contactId: string, folderId: string | null) =>
|
||||
apiPut<{ id: string; folder_id: string | null }>(`/contact-folders/contacts/${contactId}/move`, { folder_id: folderId });
|
||||
|
||||
// ── Tree builder ──
|
||||
|
||||
export function buildFolderTree(folders: ContactFolder[]): ContactFolderTreeNode[] {
|
||||
const folderMap = new Map<string, ContactFolderTreeNode>();
|
||||
const roots: ContactFolderTreeNode[] = [];
|
||||
|
||||
for (const f of folders) {
|
||||
folderMap.set(f.id, { ...f, children: [] });
|
||||
}
|
||||
|
||||
for (const f of folders) {
|
||||
const node = folderMap.get(f.id)!;
|
||||
if (f.parent_id && folderMap.has(f.parent_id)) {
|
||||
folderMap.get(f.parent_id)!.children.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
@@ -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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user