diff --git a/PROGRESS.md b/PROGRESS.md
index 551cccb..be08160 100644
--- a/PROGRESS.md
+++ b/PROGRESS.md
@@ -12,7 +12,7 @@
| 0.1 | ✅ done | 2026-07-23 | codebase-vs-requirements.md neu geschrieben, security-review-phase2.md Resolution Summary, architecture.md Implementation Status, MASTER-PLAN.md + PROGRESS.md erstellt |
| 0.2 | ✅ done | 2026-07-23 | lucide-react installieren + Icons migrieren |
| 0.3 | ✅ done | 2026-07-23 | date-fns installieren + Datum-Formatierung |
-| 0.4 | ⬜ pending | | hooks.ts aufteilen |
+| 0.4 | ✅ done | 2026-07-23 | hooks.ts aufteilen — 1298 Zeilen → 12 Module + Re-Export-Hub |
| 0.5 | ⬜ pending | | Store-Verzeichnis konsolidieren |
| 0.6 | ⬜ pending | | Frontend-Bestandsanalyse als Dokument speichern |
| 0.7 | ⬜ pending | | UI-Design-Richtlinien erstellen |
diff --git a/frontend/src/api/attachments.ts b/frontend/src/api/attachments.ts
new file mode 100644
index 0000000..e401e0a
--- /dev/null
+++ b/frontend/src/api/attachments.ts
@@ -0,0 +1,123 @@
+/**
+ * Attachment and address hooks.
+ */
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { apiGet, apiPost, apiPatch, apiDelete, apiClient } from './client';
+
+export interface Attachment {
+ id: string;
+ entity_type: string;
+ entity_id: string;
+ filename: string;
+ file_path: string;
+ mime_type: string;
+ file_size: number;
+ uploaded_by?: string | null;
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface Address {
+ id: string;
+ entity_type: string;
+ entity_id: string;
+ label: string;
+ address_type: string;
+ street: string | null;
+ street_number: string | null;
+ city: string | null;
+ zip: string | null;
+ state: string | null;
+ country: string | null;
+ is_default: boolean;
+ created_at?: string;
+ updated_at?: string;
+}
+
+// Attachments
+export function useAttachments(entityType?: string, entityId?: string) {
+ return useQuery({
+ queryKey: ['attachments', entityType, entityId],
+ queryFn: () => {
+ const params = new URLSearchParams();
+ if (entityType) params.set('entity_type', entityType);
+ if (entityId) params.set('entity_id', entityId);
+ return apiGet<{ items: Attachment[]; total: number }>(`/attachments?${params.toString()}`);
+ },
+ enabled: !!entityType && !!entityId,
+ });
+}
+
+export function useUploadAttachment() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ file, entityType, entityId }: { file: File; entityType: string; entityId: string }) => {
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('entity_type', entityType);
+ formData.append('entity_id', entityId);
+ const response = await apiClient.post('/attachments', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+ return response.data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['attachments'] });
+ },
+ });
+}
+
+export function useDeleteAttachment() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: string) => apiDelete(`/attachments/${id}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['attachments'] });
+ },
+ });
+}
+
+// Addresses
+export function useAddresses(entityType?: string, entityId?: string) {
+ return useQuery({
+ queryKey: ['addresses', entityType, entityId],
+ queryFn: () =>
+ apiGet<{ items: Address[]; total: number }>(
+ `/addresses?entity_type=${entityType}&entity_id=${entityId}`
+ ),
+ enabled: !!entityType && !!entityId,
+ });
+}
+
+export function useCreateAddress() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: Partial
& { entity_type: string; entity_id: string }) =>
+ apiPost('/addresses', data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['addresses'] });
+ },
+ });
+}
+
+export function useUpdateAddress() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ id, data }: { id: string; data: Partial }) =>
+ apiPatch(`/addresses/${id}`, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['addresses'] });
+ },
+ });
+}
+
+export function useDeleteAddress() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: string) => apiDelete(`/addresses/${id}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['addresses'] });
+ },
+ });
+}
diff --git a/frontend/src/api/audit.ts b/frontend/src/api/audit.ts
new file mode 100644
index 0000000..cf8ec4b
--- /dev/null
+++ b/frontend/src/api/audit.ts
@@ -0,0 +1,32 @@
+/**
+ * Audit log hooks.
+ */
+
+import { useQuery } from '@tanstack/react-query';
+import { apiGet } from './client';
+import { PaginatedResponse } from './types';
+
+export interface AuditLogEntry {
+ id: string;
+ timestamp: string;
+ user: string;
+ action: string;
+ entity: string;
+ entity_id?: string;
+ details?: string;
+}
+
+export function useAuditLog(page = 1, pageSize = 25, filters?: { user?: string; action?: string; entity?: string; dateFrom?: string; dateTo?: string }) {
+ const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
+ if (filters?.user) params.set('user', filters.user);
+ if (filters?.action) params.set('action', filters.action);
+ if (filters?.entity) params.set('entity', filters.entity);
+ if (filters?.dateFrom) params.set('date_from', filters.dateFrom);
+ if (filters?.dateTo) params.set('date_to', filters.dateTo);
+ return useQuery({
+ queryKey: ['auditLog', page, pageSize, filters],
+ queryFn: () =>
+ apiGet>(`/audit-log?${params.toString()}`),
+ retry: false,
+ });
+}
diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts
new file mode 100644
index 0000000..eec4f03
--- /dev/null
+++ b/frontend/src/api/auth.ts
@@ -0,0 +1,95 @@
+/**
+ * Authentication hooks: login, logout, current user, password reset, tenant switching.
+ */
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { apiPost, apiGet, setCsrfToken } from './client';
+import { useAuthStore } from '@/store/authStore';
+
+export interface LoginPayload {
+ email: string;
+ password: string;
+}
+
+export interface PasswordResetRequestPayload {
+ email: string;
+}
+
+export interface PasswordResetConfirmPayload {
+ token: string;
+ password: string;
+}
+
+export function useLogin() {
+ const { setUser, setError } = useAuthStore();
+ return useMutation({
+ mutationFn: (payload: LoginPayload) =>
+ apiPost('/auth/login', payload),
+ onSuccess: (data: any) => {
+ setUser(data.user || data);
+ setError(null);
+ // Store CSRF token for subsequent unsafe requests
+ if (data.csrf_token) {
+ setCsrfToken(data.csrf_token);
+ }
+ },
+ onError: (error: any) => {
+ setError(error.message || 'Login failed');
+ },
+ });
+}
+
+export function useLogout() {
+ const { logout } = useAuthStore();
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: () => apiPost('/auth/logout'),
+ onSettled: () => {
+ logout();
+ setCsrfToken(null);
+ queryClient.clear();
+ },
+ });
+}
+
+export function useCurrentUser() {
+ const { setUser, setAuthenticated } = useAuthStore();
+ return useQuery({
+ queryKey: ['currentUser'],
+ queryFn: async () => {
+ const data = await apiGet('/auth/me');
+ setUser(data.user || data);
+ setAuthenticated(true);
+ return data;
+ },
+ retry: false,
+ staleTime: 5 * 60 * 1000,
+ });
+}
+
+export function usePasswordResetRequest() {
+ return useMutation({
+ mutationFn: (payload: PasswordResetRequestPayload) =>
+ apiPost('/auth/password-reset/request', payload),
+ });
+}
+
+export function usePasswordResetConfirm() {
+ return useMutation({
+ mutationFn: (payload: PasswordResetConfirmPayload) =>
+ apiPost('/auth/password-reset/confirm', payload),
+ });
+}
+
+export function useSwitchTenant() {
+ const { setTenant } = useAuthStore();
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (tenantId: string) =>
+ apiPost('/auth/switch-tenant', { tenant_id: tenantId }),
+ onSuccess: (data: any) => {
+ setTenant(data.tenant || data);
+ queryClient.invalidateQueries();
+ },
+ });
+}
diff --git a/frontend/src/api/contacts.ts b/frontend/src/api/contacts.ts
new file mode 100644
index 0000000..c3182ba
--- /dev/null
+++ b/frontend/src/api/contacts.ts
@@ -0,0 +1,111 @@
+/**
+ * Contact CRUD hooks (legacy contact model — not unified contacts).
+ */
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { apiGet, apiPost, apiPatch, apiDelete, apiPut } from './client';
+import { PaginatedResponse, Contact, ContactDetail } from './types';
+import { ContactFolder } from './contactFolders';
+
+export function useContacts(page = 1, pageSize = 25, search?: string) {
+ const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
+ if (search) params.set('search', search);
+ return useQuery({
+ queryKey: ['contacts', page, pageSize, search],
+ queryFn: () =>
+ apiGet>(`/contacts?${params.toString()}`),
+ });
+}
+
+export function useContact(id?: string) {
+ return useQuery({
+ queryKey: ['contacts', id],
+ queryFn: () => apiGet(`/contacts/${id}`),
+ enabled: !!id,
+ });
+}
+
+export function useCreateContact() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: Partial & { company_ids?: string[] }) =>
+ apiPost('/contacts', data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['contacts'] });
+ },
+ });
+}
+
+export function useUpdateContact() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ id, data }: { id: string; data: Partial & { company_ids?: string[] } }) =>
+ apiPatch(`/contacts/${id}`, data),
+ onSuccess: (_data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ['contacts'] });
+ queryClient.invalidateQueries({ queryKey: ['contacts', variables.id] });
+ },
+ });
+}
+
+export function useDeleteContact() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: string) => apiDelete(`/contacts/${id}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['contacts'] });
+ },
+ });
+}
+
+// ── Contact Folders ──
+
+export function useContactFolders() {
+ return useQuery({
+ queryKey: ['contactFolders'],
+ queryFn: () => apiGet('/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'] });
+ },
+ });
+}
diff --git a/frontend/src/api/groups.ts b/frontend/src/api/groups.ts
new file mode 100644
index 0000000..f5a87d9
--- /dev/null
+++ b/frontend/src/api/groups.ts
@@ -0,0 +1,99 @@
+/**
+ * Group and group member hooks.
+ */
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { apiGet, apiPost, apiPatch, apiDelete } from './client';
+
+export interface Group {
+ id: string;
+ name: string;
+ description: string | null;
+ permissions: Record;
+ denied_permissions: string[];
+ field_permissions: Record;
+ permission_version: number;
+}
+
+export interface GroupMember {
+ user_id: string;
+ email: string;
+ name: string;
+ is_active: boolean;
+}
+
+export function useGroups() {
+ return useQuery({
+ queryKey: ['groups'],
+ queryFn: () => apiGet<{ items: Group[] }>('/groups'),
+ });
+}
+
+export function useCreateGroup() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: Partial) => apiPost('/groups', data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['groups'] });
+ },
+ });
+}
+
+export function useUpdateGroup() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ id, data }: { id: string; data: Partial }) =>
+ apiPatch(`/groups/${id}`, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['groups'] });
+ },
+ });
+}
+
+export function useDeleteGroup() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: string) => apiDelete(`/groups/${id}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['groups'] });
+ },
+ });
+}
+
+export function useGroupMembers(groupId: string | null) {
+ return useQuery({
+ queryKey: ['groupMembers', groupId],
+ queryFn: () => apiGet<{ items: GroupMember[] }>(`/groups/${groupId}/members`),
+ enabled: !!groupId,
+ });
+}
+
+export function useAddGroupMember() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
+ apiPost(`/groups/${groupId}/members`, { user_id: userId }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['groupMembers'] });
+ },
+ });
+}
+
+export function useRemoveGroupMember() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
+ apiDelete(`/groups/${groupId}/members/${userId}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['groupMembers'] });
+ },
+ });
+}
+
+export function useUserGroups(userId: string | null) {
+ return useQuery({
+ queryKey: ['userGroups', userId],
+ queryFn: () => apiGet<{ items: Group[] }>(`/groups/user/${userId}`),
+ enabled: !!userId,
+ });
+}
diff --git a/frontend/src/api/hooks.ts b/frontend/src/api/hooks.ts
index 5b6367f..5b54fd1 100644
--- a/frontend/src/api/hooks.ts
+++ b/frontend/src/api/hooks.ts
@@ -1,70 +1,31 @@
+/**
+ * hooks.ts — Re-Export Hub
+ *
+ * This file re-exports all hooks from their dedicated modules so that
+ * existing imports `from '@/api/hooks'` continue to work without change.
+ *
+ * Company hooks remain inline here — they will be removed in Phase 1.
+ */
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { apiPost, apiGet, apiPatch, apiPut, apiDelete, apiClient, setCsrfToken } from './client';
-import { useAuthStore } from '@/store/authStore';
+import { apiGet, apiPost, apiPatch, apiDelete, apiClient } from './client';
+import { PaginatedResponse, Company, CompanyDetail } from './types';
-export interface LoginPayload {
- email: string;
- password: string;
-}
+// ── Re-exports from dedicated modules ──
+export * from './types';
+export * from './auth';
+export * from './users';
+export * from './contacts';
+export * from './roles';
+export * from './groups';
+export * from './audit';
+export * from './notifications';
+export * from './plugins';
+export * from './settings';
+export * from './attachments';
+export * from './unifiedContacts';
-export interface PasswordResetRequestPayload {
- email: string;
-}
-
-export interface PasswordResetConfirmPayload {
- token: string;
- password: string;
-}
-
-export interface PaginatedResponse {
- items: T[];
- total: number;
- page: number;
- page_size: number;
-}
-
-export interface Company {
- id: string;
- name: string;
- account_number?: string | null;
- industry?: string | null;
- phone?: string | null;
- email?: string | null;
- website?: string | null;
- description?: string | null;
- created_at?: string;
- updated_at?: string;
-}
-
-export interface CompanyDetail extends Company {
- contacts?: Contact[];
-}
-
-export interface Contact {
- id: string;
- first_name: string;
- last_name: string;
- email: string;
- phone?: string | null;
- position?: string | null;
- company_ids?: string[];
- created_at?: string;
- updated_at?: string;
-}
-
-export interface ContactDetail extends Contact {
- companies?: Company[];
-}
-
-export interface AuditLogEntry {
- id: string;
- timestamp: string;
- user: string;
- action: string;
- entity: string;
- entity_id?: string;
- details?: string;
-}
+// ── Search hook (uses dynamic import, kept inline) ──
export interface SearchResult {
type: 'company' | 'contact' | 'mail' | 'file' | 'event';
@@ -74,164 +35,21 @@ export interface SearchResult {
url: string;
}
-export interface Role {
- id: string;
- name: string;
- permissions: Record;
- field_permissions?: Record;
-}
-
-export interface PermissionItem {
- key: string;
- label: string;
- category: string;
- plugin_name?: string;
-}
-
-export interface FieldDefinition {
- module: string;
- field: string;
- label: string;
- sensitivity: string;
-}
-
-export interface PermissionsResponse {
- system: PermissionItem[];
- plugins: PermissionItem[];
- all: PermissionItem[];
- field_definitions?: FieldDefinition[];
-}
-
-export interface Plugin {
- name: string;
- display_name?: string;
- description?: string;
- version?: string;
- status: 'discovered' | 'installed' | 'active' | 'inactive';
- installed?: boolean;
- active?: boolean;
-}
-
-export function useLogin() {
- const { setUser, setError } = useAuthStore();
- return useMutation({
- mutationFn: (payload: LoginPayload) =>
- apiPost('/auth/login', payload),
- onSuccess: (data: any) => {
- setUser(data.user || data);
- setError(null);
- // Store CSRF token for subsequent unsafe requests
- if (data.csrf_token) {
- setCsrfToken(data.csrf_token);
- }
- },
- onError: (error: any) => {
- setError(error.message || 'Login failed');
- },
- });
-}
-
-export function useLogout() {
- const { logout } = useAuthStore();
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: () => apiPost('/auth/logout'),
- onSettled: () => {
- logout();
- setCsrfToken(null);
- queryClient.clear();
- },
- });
-}
-
-export function useCurrentUser() {
- const { setUser, setAuthenticated } = useAuthStore();
+export function useGlobalSearch(query: string, entityTypes?: string[]) {
return useQuery({
- queryKey: ['currentUser'],
- queryFn: async () => {
- const data = await apiGet('/auth/me');
- setUser(data.user || data);
- setAuthenticated(true);
- return data;
+ queryKey: ['globalSearch', query, entityTypes],
+ queryFn: async (): Promise => {
+ if (!query.trim()) return [];
+ const { search } = await import('@/api/search');
+ const response = await search(query, entityTypes, 20);
+ return response.results || [];
},
- retry: false,
- staleTime: 5 * 60 * 1000,
+ enabled: query.trim().length > 0,
+ staleTime: 30 * 1000,
});
}
-export function usePasswordResetRequest() {
- return useMutation({
- mutationFn: (payload: PasswordResetRequestPayload) =>
- apiPost('/auth/password-reset/request', payload),
- });
-}
-
-export function usePasswordResetConfirm() {
- return useMutation({
- mutationFn: (payload: PasswordResetConfirmPayload) =>
- apiPost('/auth/password-reset/confirm', payload),
- });
-}
-
-export function useSwitchTenant() {
- const { setTenant } = useAuthStore();
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (tenantId: string) =>
- apiPost('/auth/switch-tenant', { tenant_id: tenantId }),
- onSuccess: (data: any) => {
- setTenant(data.tenant || data);
- queryClient.invalidateQueries();
- },
- });
-}
-
-export function useUsers(page = 1, pageSize = 25) {
- return useQuery({
- queryKey: ['users', page, pageSize],
- queryFn: () =>
- apiGet>(`/users?page=${page}&page_size=${pageSize}`),
- });
-}
-
-export function useUser(id?: string) {
- return useQuery({
- queryKey: ['users', id],
- queryFn: () => apiGet(`/users/${id}`),
- enabled: !!id,
- });
-}
-
-export function useCreateUser() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: any) => apiPost('/users', data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['users'] });
- },
- });
-}
-
-export function useUpdateUser() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ id, data }: { id: string; data: any }) =>
- apiPatch(`/users/${id}`, data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['users'] });
- },
- });
-}
-
-export function useDeleteUser() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (id: string) => apiDelete(`/users/${id}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['users'] });
- },
- });
-}
+// ── Company hooks (will be removed in Phase 1) ──
export function useCompanies(page = 1, pageSize = 25, search?: string, industry?: string, sortBy?: string, sortOrder?: string) {
const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
@@ -316,983 +134,3 @@ export function useCompanyImport() {
},
});
}
-
-export function useContacts(page = 1, pageSize = 25, search?: string) {
- const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
- if (search) params.set('search', search);
- return useQuery({
- queryKey: ['contacts', page, pageSize, search],
- queryFn: () =>
- apiGet>(`/contacts?${params.toString()}`),
- });
-}
-
-export function useContact(id?: string) {
- return useQuery({
- queryKey: ['contacts', id],
- queryFn: () => apiGet(`/contacts/${id}`),
- enabled: !!id,
- });
-}
-
-export function useCreateContact() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: Partial & { company_ids?: string[] }) =>
- apiPost('/contacts', data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['contacts'] });
- },
- });
-}
-
-export function useUpdateContact() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ id, data }: { id: string; data: Partial & { company_ids?: string[] } }) =>
- apiPatch(`/contacts/${id}`, data),
- onSuccess: (_data, variables) => {
- queryClient.invalidateQueries({ queryKey: ['contacts'] });
- queryClient.invalidateQueries({ queryKey: ['contacts', variables.id] });
- },
- });
-}
-
-export function useDeleteContact() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (id: string) => apiDelete(`/contacts/${id}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['contacts'] });
- },
- });
-}
-
-export function useAuditLog(page = 1, pageSize = 25, filters?: { user?: string; action?: string; entity?: string; dateFrom?: string; dateTo?: string }) {
- const params = new URLSearchParams({ page: String(page), page_size: String(pageSize) });
- if (filters?.user) params.set('user', filters.user);
- if (filters?.action) params.set('action', filters.action);
- if (filters?.entity) params.set('entity', filters.entity);
- if (filters?.dateFrom) params.set('date_from', filters.dateFrom);
- if (filters?.dateTo) params.set('date_to', filters.dateTo);
- return useQuery({
- queryKey: ['auditLog', page, pageSize, filters],
- queryFn: () =>
- apiGet>(`/audit-log?${params.toString()}`),
- retry: false,
- });
-}
-
-export function useGlobalSearch(query: string, entityTypes?: string[]) {
- return useQuery({
- queryKey: ['globalSearch', query, entityTypes],
- queryFn: async (): Promise => {
- if (!query.trim()) return [];
- const { search } = await import('@/api/search');
- const response = await search(query, entityTypes, 20);
- return response.results || [];
- },
- enabled: query.trim().length > 0,
- staleTime: 30 * 1000,
- });
-}
-
-export function useNotifications() {
- return useQuery({
- queryKey: ['notifications'],
- queryFn: () => apiGet>('/notifications'),
- });
-}
-
-export function useUnreadNotificationCount() {
- return useQuery({
- queryKey: ['notifications', 'unread-count'],
- queryFn: () => apiGet('/notifications/unread-count'),
- });
-}
-
-export function useMarkNotificationRead() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (id: string) => apiPatch(`/notifications/${id}/read`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['notifications'] });
- },
- });
-}
-
-export function useDeleteNotification() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (id: string) => apiDelete(`/notifications/${id}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['notifications'] });
- },
- });
-}
-
-export interface NotificationTypeItem {
- type_key: string;
- plugin_name: string;
- category: string;
- label: string;
- description: string | null;
- is_enabled_by_default: boolean;
- is_enabled: boolean;
-}
-
-export function useNotificationTypes() {
- return useQuery({
- queryKey: ['notification-types'],
- queryFn: () => apiGet<{ items: NotificationTypeItem[] }>('/notifications/types'),
- });
-}
-
-export function useNotificationPreferences() {
- return useQuery({
- queryKey: ['notification-preferences'],
- queryFn: () => apiGet<{ items: { type_key: string; is_enabled: boolean }[] }>('/notifications/preferences'),
- });
-}
-
-export function useUpdateNotificationPreference() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ typeKey, isEnabled }: { typeKey: string; isEnabled: boolean }) =>
- apiPatch(`/notifications/preferences/${typeKey}`, { is_enabled: isEnabled }),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['notification-preferences'] });
- queryClient.invalidateQueries({ queryKey: ['notification-types'] });
- },
- });
-}
-
-export function usePlugins() {
- return useQuery({
- queryKey: ['plugins'],
- queryFn: async () => {
- const data = await apiGet('/plugins');
- return data;
- },
- });
-}
-
-export function useInstallPlugin() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (name: string) => apiPost(`/plugins/${name}/install`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['plugins'] });
- },
- });
-}
-
-export function useActivatePlugin() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (name: string) => apiPost(`/plugins/${name}/activate`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['plugins'] });
- },
- });
-}
-
-export function useDeactivatePlugin() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (name: string) => apiPost(`/plugins/${name}/deactivate`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['plugins'] });
- },
- });
-}
-
-export function useUninstallPlugin() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ name, removeData }: { name: string; removeData: boolean }) =>
- apiDelete(`/plugins/${name}?remove_data=${removeData}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['plugins'] });
- },
- });
-}
-
-export function useRoles() {
- return useQuery({
- queryKey: ['roles'],
- queryFn: async () => {
- const data = await apiGet('/roles');
- return data;
- },
- });
-}
-
-export function usePermissions() {
- return useQuery({
- queryKey: ['permissions'],
- queryFn: () => apiGet('/roles/permissions'),
- });
-}
-
-export function useCreateRole() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: { name: string; permissions: Record; field_permissions: Record }) =>
- apiPost('/roles', data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['roles'] });
- },
- });
-}
-
-export function useUpdateRole() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ id, data }: { id: string; data: { name?: string; permissions?: Record; field_permissions?: Record } }) =>
- apiPatch(`/roles/${id}`, data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['roles'] });
- },
- });
-}
-
-export function useDeleteRole() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (id: string) => apiDelete(`/roles/${id}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['roles'] });
- },
- });
-}
-
-// ════════════════════════════════════════════════════════════════════════════
-
-// ════════════════════════════════════════════════════════════════════════════
-// Unified Contact (Rentman-style) hooks
-// ════════════════════════════════════════════════════════════════════════════
-
-export interface ContactPerson {
- id: string;
- contact_id: string;
- displayname: string;
- firstname?: string | null;
- middle_name?: string | null;
- lastname?: string | null;
- function?: string | null;
- phone?: string | null;
- mobilephone?: string | null;
- email?: string | null;
- street?: string | null;
- number?: string | null;
- postalcode?: string | null;
- city?: string | null;
- state?: string | null;
- country?: string | null;
- tags?: string | null;
- custom?: Record | null;
- created_at?: string;
- updated_at?: string;
-}
-
-export interface UnifiedContact {
- id: string;
- type: 'company' | 'person';
- displayname: string;
- name?: string | null;
- firstname?: string | null;
- surname?: string | null;
- surfix?: string | null;
- ext_name_line?: string | null;
- gender?: string | null;
- code?: string | null;
- accounting_code?: string | null;
- vendor_accounting_code?: string | null;
- // Mailing address
- mailing_street?: string | null;
- mailing_number?: string | null;
- mailing_unit_number?: string | null;
- mailing_district?: string | null;
- mailing_extra_address_line?: string | null;
- mailing_postalcode?: string | null;
- mailing_city?: string | null;
- mailing_state?: string | null;
- mailing_country?: string | null;
- // Visit address
- visit_street?: string | null;
- visit_number?: string | null;
- visit_unit_number?: string | null;
- visit_district?: string | null;
- visit_extra_address_line?: string | null;
- visit_postalcode?: string | null;
- visit_city?: string | null;
- visit_state?: string | null;
- // Invoice address
- invoice_street?: string | null;
- invoice_number?: string | null;
- invoice_unit_number?: string | null;
- invoice_district?: string | null;
- invoice_extra_address_line?: string | null;
- invoice_postalcode?: string | null;
- invoice_city?: string | null;
- invoice_state?: string | null;
- invoice_country?: string | null;
- country?: string | null;
- // Communication
- phone_1?: string | null;
- phone_2?: string | null;
- email_1?: string | null;
- email_2?: string | null;
- website?: string | null;
- // Financial
- vat_code?: string | null;
- fiscal_code?: string | null;
- commerce_code?: string | null;
- purchase_number?: string | null;
- bic?: string | null;
- bank_account?: string | null;
- // Discounts
- discount_crew: number;
- discount_transport: number;
- discount_rental: number;
- discount_sale: number;
- discount_subrent: number;
- discount_total: number;
- // Geo
- latitude?: number | null;
- longitude?: number | null;
- // Notes
- projectnote?: string | null;
- projectnote_title?: string | null;
- contact_warning?: string | null;
- tags?: string | null;
- image?: string | null;
- custom?: Record | null;
- folder_id?: string | null;
- default_person_id?: string | null;
- admin_contactperson_id?: string | null;
- contact_persons?: ContactPerson[];
- created_at?: string;
- updated_at?: string;
-}
-
-export function useUnifiedContacts(
- page = 1,
- pageSize = 25,
- search?: string,
- 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, folderId],
- queryFn: () =>
- apiGet>(`/contacts?${params.toString()}`),
- });
-}
-
-export function useUnifiedContact(id?: string) {
- return useQuery({
- queryKey: ['unifiedContacts', id],
- queryFn: () => apiGet(`/contacts/${id}`),
- enabled: !!id,
- });
-}
-
-export function useCreateUnifiedContact() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: Partial) => apiPost('/contacts', data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
- },
- });
-}
-
-export function useUpdateUnifiedContact() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ id, data }: { id: string; data: Partial }) =>
- apiPut(`/contacts/${id}`, data),
- onSuccess: (_data, variables) => {
- queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
- queryClient.invalidateQueries({ queryKey: ['unifiedContacts', variables.id] });
- },
- });
-}
-
-export function useDeleteUnifiedContact() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ id, hard }: { id: string; hard?: boolean }) =>
- apiDelete(`/contacts/${id}${hard ? '?hard=true' : ''}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
- },
- });
-}
-
-export function useContactPersons(contactId?: string) {
- return useQuery({
- queryKey: ['unifiedContacts', contactId, 'persons'],
- queryFn: () => apiGet<{ items: ContactPerson[] }>(`/contacts/${contactId}/persons`),
- enabled: !!contactId,
- });
-}
-
-export function useCreateContactPerson() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ contactId, data }: { contactId: string; data: Partial }) =>
- apiPost(`/contacts/${contactId}/persons`, data),
- onSuccess: (_data, variables) => {
- queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
- queryClient.invalidateQueries({ queryKey: ['unifiedContacts', variables.contactId] });
- queryClient.invalidateQueries({ queryKey: ['unifiedContacts', variables.contactId, 'persons'] });
- },
- });
-}
-
-export function useUpdateContactPerson() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ contactId, personId, data }: { contactId: string; personId: string; data: Partial }) =>
- apiPut(`/contacts/${contactId}/persons/${personId}`, data),
- onSuccess: (_data, variables) => {
- queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
- queryClient.invalidateQueries({ queryKey: ['unifiedContacts', variables.contactId] });
- queryClient.invalidateQueries({ queryKey: ['unifiedContacts', variables.contactId, 'persons'] });
- },
- });
-}
-
-export function useDeleteContactPerson() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ contactId, personId }: { contactId: string; personId: string }) =>
- apiDelete(`/contacts/${contactId}/persons/${personId}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
- },
- });
-}
-
-// Standalone API functions (for non-hook usage)
-export async function fetchContacts(
- page = 1,
- pageSize = 25,
- search?: string,
- contactType?: string,
- sortBy?: string,
- sortOrder?: string,
-): Promise> {
- 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);
- return apiGet>(`/contacts?${params.toString()}`);
-}
-
-export async function fetchContact(id: string): Promise {
- return apiGet(`/contacts/${id}`);
-}
-
-export async function createContact(data: Partial): Promise {
- return apiPost('/contacts', data);
-}
-
-export async function updateContact(id: string, data: Partial): Promise {
- return apiPut(`/contacts/${id}`, data);
-}
-
-export async function deleteContact(id: string, hard?: boolean): Promise {
- await apiDelete(`/contacts/${id}${hard ? '?hard=true' : ''}`);
-}
-
-export async function fetchContactPersons(contactId: string): Promise<{ items: ContactPerson[] }> {
- return apiGet<{ items: ContactPerson[] }>(`/contacts/${contactId}/persons`);
-}
-
-export async function createContactPerson(contactId: string, data: Partial): Promise {
- return apiPost(`/contacts/${contactId}/persons`, data);
-}
-
-export async function updateContactPerson(contactId: string, personId: string, data: Partial): Promise {
- return apiPut(`/contacts/${contactId}/persons/${personId}`, data);
-}
-
-export async function deleteContactPerson(contactId: string, personId: string): Promise {
- await apiDelete(`/contacts/${contactId}/persons/${personId}`);
-}
-
-// ════════════════════════════════════════════════════════════════════════════
-// System Settings, Currency, Tax, Sequence hooks
-// ════════════════════════════════════════════════════════════════════════════
-
-export interface Currency {
- id: string;
- code: string;
- name: string;
- symbol: string;
- is_default: boolean;
- created_at?: string;
- updated_at?: string;
-}
-
-export interface TaxRate {
- id: string;
- name: string;
- rate: number;
- is_default: boolean;
- country?: string | null;
- created_at?: string;
- updated_at?: string;
-}
-
-export interface Sequence {
- id: string;
- name: string;
- prefix: string;
- next_number: number;
- padding: number;
- created_at?: string;
- updated_at?: string;
-}
-
-export interface SystemSettings {
- id?: string;
- company_name: string;
- company_legal_form?: string | null;
- company_street: string;
- company_city: string;
- company_zip: string;
- company_country: string;
- tax_number?: string | null;
- vat_id?: string | null;
- iban?: string | null;
- bic?: string | null;
- bank_name?: string | null;
- ceo?: string | null;
- trade_register?: string | null;
- default_currency_id?: string | null;
- default_tax_id?: string | null;
- invoice_prefix: string;
- quote_prefix: string;
- payment_terms_days: number;
- created_at?: string;
- updated_at?: string;
-}
-
-// System Settings
-export function useSystemSettings() {
- return useQuery({
- queryKey: ['systemSettings'],
- queryFn: () => apiGet('/system-settings'),
- });
-}
-
-export function useUpdateSystemSettings() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: Partial) => apiClient.put('/system-settings', data).then(r => r.data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['systemSettings'] });
- },
- });
-}
-
-// Currencies
-export function useCurrencies() {
- return useQuery({
- queryKey: ['currencies'],
- queryFn: () => apiGet<{ items: Currency[]; total: number }>('/currencies'),
- });
-}
-
-export function useCreateCurrency() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: Partial) => apiPost('/currencies', data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['currencies'] });
- },
- });
-}
-
-export function useUpdateCurrency() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ id, data }: { id: string; data: Partial }) =>
- apiPatch(`/currencies/${id}`, data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['currencies'] });
- },
- });
-}
-
-export function useDeleteCurrency() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (id: string) => apiDelete(`/currencies/${id}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['currencies'] });
- },
- });
-}
-
-// Taxes
-export function useTaxes() {
- return useQuery({
- queryKey: ['taxes'],
- queryFn: () => apiGet<{ items: TaxRate[]; total: number }>('/taxes'),
- });
-}
-
-export function useCreateTax() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: Partial) => apiPost('/taxes', data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['taxes'] });
- },
- });
-}
-
-export function useUpdateTax() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ id, data }: { id: string; data: Partial }) =>
- apiPatch(`/taxes/${id}`, data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['taxes'] });
- },
- });
-}
-
-export function useDeleteTax() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (id: string) => apiDelete(`/taxes/${id}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['taxes'] });
- },
- });
-}
-
-// Sequences
-export function useSequences() {
- return useQuery({
- queryKey: ['sequences'],
- queryFn: () => apiGet<{ items: Sequence[]; total: number }>('/sequences'),
- });
-}
-
-export function useCreateSequence() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: Partial) => apiPost('/sequences', data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['sequences'] });
- },
- });
-}
-
-export function useUpdateSequence() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ id, data }: { id: string; data: Partial }) =>
- apiPatch(`/sequences/${id}`, data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['sequences'] });
- },
- });
-}
-
-export function useDeleteSequence() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (id: string) => apiDelete(`/sequences/${id}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['sequences'] });
- },
- });
-}
-
-// Attachments
-export interface Attachment {
- id: string;
- entity_type: string;
- entity_id: string;
- filename: string;
- file_path: string;
- mime_type: string;
- file_size: number;
- uploaded_by?: string | null;
- created_at?: string;
- updated_at?: string;
-}
-
-export function useAttachments(entityType?: string, entityId?: string) {
- return useQuery({
- queryKey: ['attachments', entityType, entityId],
- queryFn: () => {
- const params = new URLSearchParams();
- if (entityType) params.set('entity_type', entityType);
- if (entityId) params.set('entity_id', entityId);
- return apiGet<{ items: Attachment[]; total: number }>(`/attachments?${params.toString()}`);
- },
- enabled: !!entityType && !!entityId,
- });
-}
-
-export function useUploadAttachment() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: async ({ file, entityType, entityId }: { file: File; entityType: string; entityId: string }) => {
- const formData = new FormData();
- formData.append('file', file);
- formData.append('entity_type', entityType);
- formData.append('entity_id', entityId);
- const response = await apiClient.post('/attachments', formData, {
- headers: { 'Content-Type': 'multipart/form-data' },
- });
- return response.data;
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['attachments'] });
- },
- });
-}
-
-export function useDeleteAttachment() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (id: string) => apiDelete(`/attachments/${id}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['attachments'] });
- },
- });
-}
-
-// ════════════════════════════════════════════════════════════════════════════
-// Address hooks
-// ════════════════════════════════════════════════════════════════════════════
-
-export interface Address {
- id: string;
- entity_type: string;
- entity_id: string;
- label: string;
- address_type: string;
- street: string | null;
- street_number: string | null;
- city: string | null;
- zip: string | null;
- state: string | null;
- country: string | null;
- is_default: boolean;
- created_at?: string;
- updated_at?: string;
-}
-
-export function useAddresses(entityType?: string, entityId?: string) {
- return useQuery({
- queryKey: ['addresses', entityType, entityId],
- queryFn: () =>
- apiGet<{ items: Address[]; total: number }>(
- `/addresses?entity_type=${entityType}&entity_id=${entityId}`
- ),
- enabled: !!entityType && !!entityId,
- });
-}
-
-export function useCreateAddress() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: Partial & { entity_type: string; entity_id: string }) =>
- apiPost('/addresses', data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['addresses'] });
- },
- });
-}
-
-export function useUpdateAddress() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ id, data }: { id: string; data: Partial }) =>
- apiPatch(`/addresses/${id}`, data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['addresses'] });
- },
- });
-}
-
-export function useDeleteAddress() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (id: string) => apiDelete(`/addresses/${id}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['addresses'] });
- },
- });
-}
-
-// ════════════════════════════════════════════════════════════════════════════
-// Group hooks
-// ════════════════════════════════════════════════════════════════════════════
-
-export interface Group {
- id: string;
- name: string;
- description: string | null;
- permissions: Record;
- denied_permissions: string[];
- field_permissions: Record;
- permission_version: number;
-}
-
-export interface GroupMember {
- user_id: string;
- email: string;
- name: string;
- is_active: boolean;
-}
-
-export function useGroups() {
- return useQuery({
- queryKey: ['groups'],
- queryFn: () => apiGet<{ items: Group[] }>('/groups'),
- });
-}
-
-export function useCreateGroup() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (data: Partial) => apiPost('/groups', data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['groups'] });
- },
- });
-}
-
-export function useUpdateGroup() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ id, data }: { id: string; data: Partial }) =>
- apiPatch(`/groups/${id}`, data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['groups'] });
- },
- });
-}
-
-export function useDeleteGroup() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: (id: string) => apiDelete(`/groups/${id}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['groups'] });
- },
- });
-}
-
-export function useGroupMembers(groupId: string | null) {
- return useQuery({
- queryKey: ['groupMembers', groupId],
- queryFn: () => apiGet<{ items: GroupMember[] }>(`/groups/${groupId}/members`),
- enabled: !!groupId,
- });
-}
-
-export function useAddGroupMember() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
- apiPost(`/groups/${groupId}/members`, { user_id: userId }),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['groupMembers'] });
- },
- });
-}
-
-export function useRemoveGroupMember() {
- const queryClient = useQueryClient();
- return useMutation({
- mutationFn: ({ groupId, userId }: { groupId: string; userId: string }) =>
- apiDelete(`/groups/${groupId}/members/${userId}`),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['groupMembers'] });
- },
- });
-}
-
-export function useUserGroups(userId: string | null) {
- return useQuery({
- queryKey: ['userGroups', userId],
- queryFn: () => apiGet<{ items: Group[] }>(`/groups/user/${userId}`),
- enabled: !!userId,
- });
-}
-
-// ── Contact Folders ──
-
-export function useContactFolders() {
- return useQuery({
- queryKey: ['contactFolders'],
- queryFn: () => apiGet('/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'] });
- },
- });
-}
-
diff --git a/frontend/src/api/notifications.ts b/frontend/src/api/notifications.ts
new file mode 100644
index 0000000..890b589
--- /dev/null
+++ b/frontend/src/api/notifications.ts
@@ -0,0 +1,77 @@
+/**
+ * Notification hooks: list, unread count, mark read, delete, types, preferences.
+ */
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { apiGet, apiPatch, apiDelete } from './client';
+import { PaginatedResponse } from './types';
+
+export interface NotificationTypeItem {
+ type_key: string;
+ plugin_name: string;
+ category: string;
+ label: string;
+ description: string | null;
+ is_enabled_by_default: boolean;
+ is_enabled: boolean;
+}
+
+export function useNotifications() {
+ return useQuery({
+ queryKey: ['notifications'],
+ queryFn: () => apiGet>('/notifications'),
+ });
+}
+
+export function useUnreadNotificationCount() {
+ return useQuery({
+ queryKey: ['notifications', 'unread-count'],
+ queryFn: () => apiGet('/notifications/unread-count'),
+ });
+}
+
+export function useMarkNotificationRead() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: string) => apiPatch(`/notifications/${id}/read`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['notifications'] });
+ },
+ });
+}
+
+export function useDeleteNotification() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: string) => apiDelete(`/notifications/${id}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['notifications'] });
+ },
+ });
+}
+
+export function useNotificationTypes() {
+ return useQuery({
+ queryKey: ['notification-types'],
+ queryFn: () => apiGet<{ items: NotificationTypeItem[] }>('/notifications/types'),
+ });
+}
+
+export function useNotificationPreferences() {
+ return useQuery({
+ queryKey: ['notification-preferences'],
+ queryFn: () => apiGet<{ items: { type_key: string; is_enabled: boolean }[] }>('/notifications/preferences'),
+ });
+}
+
+export function useUpdateNotificationPreference() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ typeKey, isEnabled }: { typeKey: string; isEnabled: boolean }) =>
+ apiPatch(`/notifications/preferences/${typeKey}`, { is_enabled: isEnabled }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['notification-preferences'] });
+ queryClient.invalidateQueries({ queryKey: ['notification-types'] });
+ },
+ });
+}
diff --git a/frontend/src/api/plugins.ts b/frontend/src/api/plugins.ts
new file mode 100644
index 0000000..4b9ea43
--- /dev/null
+++ b/frontend/src/api/plugins.ts
@@ -0,0 +1,67 @@
+/**
+ * Plugin management hooks: list, install, activate, deactivate, uninstall.
+ */
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { apiGet, apiPost, apiDelete } from './client';
+
+export interface Plugin {
+ name: string;
+ display_name?: string;
+ description?: string;
+ version?: string;
+ status: 'discovered' | 'installed' | 'active' | 'inactive';
+ installed?: boolean;
+ active?: boolean;
+}
+
+export function usePlugins() {
+ return useQuery({
+ queryKey: ['plugins'],
+ queryFn: async () => {
+ const data = await apiGet('/plugins');
+ return data;
+ },
+ });
+}
+
+export function useInstallPlugin() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (name: string) => apiPost(`/plugins/${name}/install`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['plugins'] });
+ },
+ });
+}
+
+export function useActivatePlugin() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (name: string) => apiPost(`/plugins/${name}/activate`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['plugins'] });
+ },
+ });
+}
+
+export function useDeactivatePlugin() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (name: string) => apiPost(`/plugins/${name}/deactivate`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['plugins'] });
+ },
+ });
+}
+
+export function useUninstallPlugin() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ name, removeData }: { name: string; removeData: boolean }) =>
+ apiDelete(`/plugins/${name}?remove_data=${removeData}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['plugins'] });
+ },
+ });
+}
diff --git a/frontend/src/api/roles.ts b/frontend/src/api/roles.ts
new file mode 100644
index 0000000..eaed23d
--- /dev/null
+++ b/frontend/src/api/roles.ts
@@ -0,0 +1,83 @@
+/**
+ * Role and permission hooks.
+ */
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { apiGet, apiPost, apiPatch, apiDelete } from './client';
+
+export interface Role {
+ id: string;
+ name: string;
+ permissions: Record;
+ field_permissions?: Record;
+}
+
+export interface PermissionItem {
+ key: string;
+ label: string;
+ category: string;
+ plugin_name?: string;
+}
+
+export interface FieldDefinition {
+ module: string;
+ field: string;
+ label: string;
+ sensitivity: string;
+}
+
+export interface PermissionsResponse {
+ system: PermissionItem[];
+ plugins: PermissionItem[];
+ all: PermissionItem[];
+ field_definitions?: FieldDefinition[];
+}
+
+export function useRoles() {
+ return useQuery({
+ queryKey: ['roles'],
+ queryFn: async () => {
+ const data = await apiGet('/roles');
+ return data;
+ },
+ });
+}
+
+export function usePermissions() {
+ return useQuery({
+ queryKey: ['permissions'],
+ queryFn: () => apiGet('/roles/permissions'),
+ });
+}
+
+export function useCreateRole() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: { name: string; permissions: Record; field_permissions: Record }) =>
+ apiPost('/roles', data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['roles'] });
+ },
+ });
+}
+
+export function useUpdateRole() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ id, data }: { id: string; data: { name?: string; permissions?: Record; field_permissions?: Record } }) =>
+ apiPatch(`/roles/${id}`, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['roles'] });
+ },
+ });
+}
+
+export function useDeleteRole() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: string) => apiDelete(`/roles/${id}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['roles'] });
+ },
+ });
+}
diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts
new file mode 100644
index 0000000..2dee837
--- /dev/null
+++ b/frontend/src/api/settings.ts
@@ -0,0 +1,195 @@
+/**
+ * System settings, currency, tax, and sequence hooks.
+ */
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { apiGet, apiPost, apiPatch, apiDelete, apiClient } from './client';
+
+export interface Currency {
+ id: string;
+ code: string;
+ name: string;
+ symbol: string;
+ is_default: boolean;
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface TaxRate {
+ id: string;
+ name: string;
+ rate: number;
+ is_default: boolean;
+ country?: string | null;
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface Sequence {
+ id: string;
+ name: string;
+ prefix: string;
+ next_number: number;
+ padding: number;
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface SystemSettings {
+ id?: string;
+ company_name: string;
+ company_legal_form?: string | null;
+ company_street: string;
+ company_city: string;
+ company_zip: string;
+ company_country: string;
+ tax_number?: string | null;
+ vat_id?: string | null;
+ iban?: string | null;
+ bic?: string | null;
+ bank_name?: string | null;
+ ceo?: string | null;
+ trade_register?: string | null;
+ default_currency_id?: string | null;
+ default_tax_id?: string | null;
+ invoice_prefix: string;
+ quote_prefix: string;
+ payment_terms_days: number;
+ created_at?: string;
+ updated_at?: string;
+}
+
+// System Settings
+export function useSystemSettings() {
+ return useQuery({
+ queryKey: ['systemSettings'],
+ queryFn: () => apiGet('/system-settings'),
+ });
+}
+
+export function useUpdateSystemSettings() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: Partial) => apiClient.put('/system-settings', data).then(r => r.data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['systemSettings'] });
+ },
+ });
+}
+
+// Currencies
+export function useCurrencies() {
+ return useQuery({
+ queryKey: ['currencies'],
+ queryFn: () => apiGet<{ items: Currency[]; total: number }>('/currencies'),
+ });
+}
+
+export function useCreateCurrency() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: Partial) => apiPost('/currencies', data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['currencies'] });
+ },
+ });
+}
+
+export function useUpdateCurrency() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ id, data }: { id: string; data: Partial }) =>
+ apiPatch(`/currencies/${id}`, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['currencies'] });
+ },
+ });
+}
+
+export function useDeleteCurrency() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: string) => apiDelete(`/currencies/${id}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['currencies'] });
+ },
+ });
+}
+
+// Taxes
+export function useTaxes() {
+ return useQuery({
+ queryKey: ['taxes'],
+ queryFn: () => apiGet<{ items: TaxRate[]; total: number }>('/taxes'),
+ });
+}
+
+export function useCreateTax() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: Partial) => apiPost('/taxes', data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['taxes'] });
+ },
+ });
+}
+
+export function useUpdateTax() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ id, data }: { id: string; data: Partial }) =>
+ apiPatch(`/taxes/${id}`, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['taxes'] });
+ },
+ });
+}
+
+export function useDeleteTax() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: string) => apiDelete(`/taxes/${id}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['taxes'] });
+ },
+ });
+}
+
+// Sequences
+export function useSequences() {
+ return useQuery({
+ queryKey: ['sequences'],
+ queryFn: () => apiGet<{ items: Sequence[]; total: number }>('/sequences'),
+ });
+}
+
+export function useCreateSequence() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: Partial) => apiPost('/sequences', data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['sequences'] });
+ },
+ });
+}
+
+export function useUpdateSequence() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ id, data }: { id: string; data: Partial }) =>
+ apiPatch(`/sequences/${id}`, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['sequences'] });
+ },
+ });
+}
+
+export function useDeleteSequence() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: string) => apiDelete(`/sequences/${id}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['sequences'] });
+ },
+ });
+}
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
new file mode 100644
index 0000000..6f54132
--- /dev/null
+++ b/frontend/src/api/types.ts
@@ -0,0 +1,43 @@
+/**
+ * Shared type definitions used across multiple API modules.
+ */
+
+export interface PaginatedResponse {
+ items: T[];
+ total: number;
+ page: number;
+ page_size: number;
+}
+
+export interface Company {
+ id: string;
+ name: string;
+ account_number?: string | null;
+ industry?: string | null;
+ phone?: string | null;
+ email?: string | null;
+ website?: string | null;
+ description?: string | null;
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface CompanyDetail extends Company {
+ contacts?: Contact[];
+}
+
+export interface Contact {
+ id: string;
+ first_name: string;
+ last_name: string;
+ email: string;
+ phone?: string | null;
+ position?: string | null;
+ company_ids?: string[];
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface ContactDetail extends Contact {
+ companies?: Company[];
+}
diff --git a/frontend/src/api/unifiedContacts.ts b/frontend/src/api/unifiedContacts.ts
new file mode 100644
index 0000000..2b9e2aa
--- /dev/null
+++ b/frontend/src/api/unifiedContacts.ts
@@ -0,0 +1,268 @@
+/**
+ * Unified Contact (Rentman-style) hooks and standalone API functions.
+ */
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { apiGet, apiPost, apiPut, apiDelete } from './client';
+import { PaginatedResponse } from './types';
+
+export interface ContactPerson {
+ id: string;
+ contact_id: string;
+ displayname: string;
+ firstname?: string | null;
+ middle_name?: string | null;
+ lastname?: string | null;
+ function?: string | null;
+ phone?: string | null;
+ mobilephone?: string | null;
+ email?: string | null;
+ street?: string | null;
+ number?: string | null;
+ postalcode?: string | null;
+ city?: string | null;
+ state?: string | null;
+ country?: string | null;
+ tags?: string | null;
+ custom?: Record | null;
+ created_at?: string;
+ updated_at?: string;
+}
+
+export interface UnifiedContact {
+ id: string;
+ type: 'company' | 'person';
+ displayname: string;
+ name?: string | null;
+ firstname?: string | null;
+ surname?: string | null;
+ surfix?: string | null;
+ ext_name_line?: string | null;
+ gender?: string | null;
+ code?: string | null;
+ accounting_code?: string | null;
+ vendor_accounting_code?: string | null;
+ // Mailing address
+ mailing_street?: string | null;
+ mailing_number?: string | null;
+ mailing_unit_number?: string | null;
+ mailing_district?: string | null;
+ mailing_extra_address_line?: string | null;
+ mailing_postalcode?: string | null;
+ mailing_city?: string | null;
+ mailing_state?: string | null;
+ mailing_country?: string | null;
+ // Visit address
+ visit_street?: string | null;
+ visit_number?: string | null;
+ visit_unit_number?: string | null;
+ visit_district?: string | null;
+ visit_extra_address_line?: string | null;
+ visit_postalcode?: string | null;
+ visit_city?: string | null;
+ visit_state?: string | null;
+ // Invoice address
+ invoice_street?: string | null;
+ invoice_number?: string | null;
+ invoice_unit_number?: string | null;
+ invoice_district?: string | null;
+ invoice_extra_address_line?: string | null;
+ invoice_postalcode?: string | null;
+ invoice_city?: string | null;
+ invoice_state?: string | null;
+ invoice_country?: string | null;
+ country?: string | null;
+ // Communication
+ phone_1?: string | null;
+ phone_2?: string | null;
+ email_1?: string | null;
+ email_2?: string | null;
+ website?: string | null;
+ // Financial
+ vat_code?: string | null;
+ fiscal_code?: string | null;
+ commerce_code?: string | null;
+ purchase_number?: string | null;
+ bic?: string | null;
+ bank_account?: string | null;
+ // Discounts
+ discount_crew: number;
+ discount_transport: number;
+ discount_rental: number;
+ discount_sale: number;
+ discount_subrent: number;
+ discount_total: number;
+ // Geo
+ latitude?: number | null;
+ longitude?: number | null;
+ // Notes
+ projectnote?: string | null;
+ projectnote_title?: string | null;
+ contact_warning?: string | null;
+ tags?: string | null;
+ image?: string | null;
+ custom?: Record | null;
+ folder_id?: string | null;
+ default_person_id?: string | null;
+ admin_contactperson_id?: string | null;
+ contact_persons?: ContactPerson[];
+ created_at?: string;
+ updated_at?: string;
+}
+
+export function useUnifiedContacts(
+ page = 1,
+ pageSize = 25,
+ search?: string,
+ 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, folderId],
+ queryFn: () =>
+ apiGet>(`/contacts?${params.toString()}`),
+ });
+}
+
+export function useUnifiedContact(id?: string) {
+ return useQuery({
+ queryKey: ['unifiedContacts', id],
+ queryFn: () => apiGet(`/contacts/${id}`),
+ enabled: !!id,
+ });
+}
+
+export function useCreateUnifiedContact() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: Partial) => apiPost('/contacts', data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
+ },
+ });
+}
+
+export function useUpdateUnifiedContact() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ id, data }: { id: string; data: Partial }) =>
+ apiPut(`/contacts/${id}`, data),
+ onSuccess: (_data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
+ queryClient.invalidateQueries({ queryKey: ['unifiedContacts', variables.id] });
+ },
+ });
+}
+
+export function useDeleteUnifiedContact() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ id, hard }: { id: string; hard?: boolean }) =>
+ apiDelete(`/contacts/${id}${hard ? '?hard=true' : ''}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
+ },
+ });
+}
+
+export function useContactPersons(contactId?: string) {
+ return useQuery({
+ queryKey: ['unifiedContacts', contactId, 'persons'],
+ queryFn: () => apiGet<{ items: ContactPerson[] }>(`/contacts/${contactId}/persons`),
+ enabled: !!contactId,
+ });
+}
+
+export function useCreateContactPerson() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ contactId, data }: { contactId: string; data: Partial }) =>
+ apiPost(`/contacts/${contactId}/persons`, data),
+ onSuccess: (_data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
+ queryClient.invalidateQueries({ queryKey: ['unifiedContacts', variables.contactId] });
+ queryClient.invalidateQueries({ queryKey: ['unifiedContacts', variables.contactId, 'persons'] });
+ },
+ });
+}
+
+export function useUpdateContactPerson() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ contactId, personId, data }: { contactId: string; personId: string; data: Partial }) =>
+ apiPut(`/contacts/${contactId}/persons/${personId}`, data),
+ onSuccess: (_data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
+ queryClient.invalidateQueries({ queryKey: ['unifiedContacts', variables.contactId] });
+ queryClient.invalidateQueries({ queryKey: ['unifiedContacts', variables.contactId, 'persons'] });
+ },
+ });
+}
+
+export function useDeleteContactPerson() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ contactId, personId }: { contactId: string; personId: string }) =>
+ apiDelete(`/contacts/${contactId}/persons/${personId}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
+ },
+ });
+}
+
+// Standalone API functions (for non-hook usage)
+export async function fetchContacts(
+ page = 1,
+ pageSize = 25,
+ search?: string,
+ contactType?: string,
+ sortBy?: string,
+ sortOrder?: string,
+): Promise> {
+ 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);
+ return apiGet>(`/contacts?${params.toString()}`);
+}
+
+export async function fetchContact(id: string): Promise {
+ return apiGet(`/contacts/${id}`);
+}
+
+export async function createContact(data: Partial): Promise {
+ return apiPost('/contacts', data);
+}
+
+export async function updateContact(id: string, data: Partial): Promise {
+ return apiPut(`/contacts/${id}`, data);
+}
+
+export async function deleteContact(id: string, hard?: boolean): Promise {
+ await apiDelete(`/contacts/${id}${hard ? '?hard=true' : ''}`);
+}
+
+export async function fetchContactPersons(contactId: string): Promise<{ items: ContactPerson[] }> {
+ return apiGet<{ items: ContactPerson[] }>(`/contacts/${contactId}/persons`);
+}
+
+export async function createContactPerson(contactId: string, data: Partial): Promise {
+ return apiPost(`/contacts/${contactId}/persons`, data);
+}
+
+export async function updateContactPerson(contactId: string, personId: string, data: Partial): Promise {
+ return apiPut(`/contacts/${contactId}/persons/${personId}`, data);
+}
+
+export async function deleteContactPerson(contactId: string, personId: string): Promise {
+ await apiDelete(`/contacts/${contactId}/persons/${personId}`);
+}
diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts
new file mode 100644
index 0000000..d29ad45
--- /dev/null
+++ b/frontend/src/api/users.ts
@@ -0,0 +1,54 @@
+/**
+ * User CRUD hooks.
+ */
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { apiGet, apiPost, apiPatch, apiDelete } from './client';
+import { PaginatedResponse } from './types';
+
+export function useUsers(page = 1, pageSize = 25) {
+ return useQuery({
+ queryKey: ['users', page, pageSize],
+ queryFn: () =>
+ apiGet>(`/users?page=${page}&page_size=${pageSize}`),
+ });
+}
+
+export function useUser(id?: string) {
+ return useQuery({
+ queryKey: ['users', id],
+ queryFn: () => apiGet(`/users/${id}`),
+ enabled: !!id,
+ });
+}
+
+export function useCreateUser() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: any) => apiPost('/users', data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['users'] });
+ },
+ });
+}
+
+export function useUpdateUser() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: ({ id, data }: { id: string; data: any }) =>
+ apiPatch(`/users/${id}`, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['users'] });
+ },
+ });
+}
+
+export function useDeleteUser() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (id: string) => apiDelete(`/users/${id}`),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['users'] });
+ },
+ });
+}