From 75d2f884dad766cb21884c9dc570ba1011cd8936 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Thu, 23 Jul 2026 05:11:16 +0200 Subject: [PATCH] Phase 0.4: split hooks.ts into separate API modules --- PROGRESS.md | 2 +- frontend/src/api/attachments.ts | 123 +++ frontend/src/api/audit.ts | 32 + frontend/src/api/auth.ts | 95 +++ frontend/src/api/contacts.ts | 111 +++ frontend/src/api/groups.ts | 99 +++ frontend/src/api/hooks.ts | 1232 +-------------------------- frontend/src/api/notifications.ts | 77 ++ frontend/src/api/plugins.ts | 67 ++ frontend/src/api/roles.ts | 83 ++ frontend/src/api/settings.ts | 195 +++++ frontend/src/api/types.ts | 43 + frontend/src/api/unifiedContacts.ts | 268 ++++++ frontend/src/api/users.ts | 54 ++ 14 files changed, 1283 insertions(+), 1198 deletions(-) create mode 100644 frontend/src/api/attachments.ts create mode 100644 frontend/src/api/audit.ts create mode 100644 frontend/src/api/auth.ts create mode 100644 frontend/src/api/contacts.ts create mode 100644 frontend/src/api/groups.ts create mode 100644 frontend/src/api/notifications.ts create mode 100644 frontend/src/api/plugins.ts create mode 100644 frontend/src/api/roles.ts create mode 100644 frontend/src/api/settings.ts create mode 100644 frontend/src/api/types.ts create mode 100644 frontend/src/api/unifiedContacts.ts create mode 100644 frontend/src/api/users.ts 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'] }); + }, + }); +}