diff --git a/frontend/src/api/hooks.ts b/frontend/src/api/hooks.ts index 22fe421..94f8191 100644 --- a/frontend/src/api/hooks.ts +++ b/frontend/src/api/hooks.ts @@ -1 +1,868 @@ -§§include(/a0/usr/chats/XttavUaL/messages/hooks_update.ts) \ No newline at end of file +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiPost, apiGet, apiPatch, apiDelete, apiClient } 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 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; +} + +export interface SearchResult { + type: 'company' | 'contact'; + id: string; + name: string; + description?: string; + 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 PermissionsResponse { + system: PermissionItem[]; + plugins: PermissionItem[]; + all: PermissionItem[]; +} + +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); + }, + 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(); + 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(); + }, + }); +} + +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'] }); + }, + }); +} + +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) }); + if (search) params.set('search', search); + if (industry) params.set('industry', industry); + if (sortBy) params.set('sort_by', sortBy); + if (sortOrder) params.set('sort_order', sortOrder); + return useQuery({ + queryKey: ['companies', page, pageSize, search, industry, sortBy, sortOrder], + queryFn: () => + apiGet>(`/companies?${params.toString()}`), + }); +} + +export function useCompany(id?: string) { + return useQuery({ + queryKey: ['companies', id], + queryFn: () => apiGet(`/companies/${id}`), + enabled: !!id, + }); +} + +export function useCreateCompany() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: Partial) => apiPost('/companies', data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['companies'] }); + }, + }); +} + +export function useUpdateCompany() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: Partial }) => + apiPatch(`/companies/${id}`, data), + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ queryKey: ['companies'] }); + queryClient.invalidateQueries({ queryKey: ['companies', variables.id] }); + }, + }); +} + +export function useDeleteCompany() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => apiDelete(`/companies/${id}`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['companies'] }); + }, + }); +} + +export function useCompanyExport(search?: string, industry?: string) { + return useMutation({ + mutationFn: async () => { + const params = new URLSearchParams({ format: 'csv' }); + if (search) params.set('search', search); + if (industry) params.set('industry', industry); + const response = await apiClient.get(`/companies/export?${params.toString()}`, { + responseType: 'blob', + }); + return response.data; + }, + }); +} + +export function useCompanyImport() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (file: File) => { + const formData = new FormData(); + formData.append('file', file); + const response = await apiClient.post('/companies/import', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + return response.data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['companies'] }); + }, + }); +} + +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 results: SearchResult[] = []; + const types = entityTypes && entityTypes.length > 0 ? entityTypes : ['company', 'contact']; + const tasks: Promise[] = []; + if (types.includes('company')) { + tasks.push( + apiGet>(`/companies?page=1&page_size=10&search=${encodeURIComponent(query)}`) + .then((data) => { + for (const item of data.items) { + results.push({ + type: 'company', + id: item.id, + name: item.name, + description: item.industry || item.email || '', + url: `/companies/${item.id}`, + }); + } + }) + .catch(() => {}) + ); + } + if (types.includes('contact')) { + tasks.push( + apiGet>(`/contacts?page=1&page_size=10&search=${encodeURIComponent(query)}`) + .then((data) => { + for (const item of data.items) { + results.push({ + type: 'contact', + id: item.id, + name: `${item.first_name} ${item.last_name}`, + description: item.email || item.phone || '', + url: `/contacts/${item.id}`, + }); + } + }) + .catch(() => {}) + ); + } + await Promise.all(tasks); + return 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 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'] }); + }, + }); +} + +// ════════════════════════════════════════════════════════════════════════════ +// 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'] }); + }, + }); +}