1243 lines
36 KiB
TypeScript
1243 lines
36 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { apiPost, apiGet, apiPatch, apiPut, apiDelete, apiClient, 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 interface PaginatedResponse<T> {
|
|
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' | 'mail' | 'file' | 'event';
|
|
id: string;
|
|
name: string;
|
|
description?: string;
|
|
url: string;
|
|
}
|
|
|
|
export interface Role {
|
|
id: string;
|
|
name: string;
|
|
permissions: Record<string, any>;
|
|
field_permissions?: Record<string, any>;
|
|
}
|
|
|
|
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();
|
|
return useQuery({
|
|
queryKey: ['currentUser'],
|
|
queryFn: async () => {
|
|
const data = await apiGet<any>('/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<PaginatedResponse<any>>(`/users?page=${page}&page_size=${pageSize}`),
|
|
});
|
|
}
|
|
|
|
export function useUser(id?: string) {
|
|
return useQuery({
|
|
queryKey: ['users', id],
|
|
queryFn: () => apiGet<any>(`/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<PaginatedResponse<Company>>(`/companies?${params.toString()}`),
|
|
});
|
|
}
|
|
|
|
export function useCompany(id?: string) {
|
|
return useQuery({
|
|
queryKey: ['companies', id],
|
|
queryFn: () => apiGet<CompanyDetail>(`/companies/${id}`),
|
|
enabled: !!id,
|
|
});
|
|
}
|
|
|
|
export function useCreateCompany() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: Partial<Company>) => apiPost('/companies', data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['companies'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateCompany() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: Partial<Company> }) =>
|
|
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<PaginatedResponse<Contact>>(`/contacts?${params.toString()}`),
|
|
});
|
|
}
|
|
|
|
export function useContact(id?: string) {
|
|
return useQuery({
|
|
queryKey: ['contacts', id],
|
|
queryFn: () => apiGet<ContactDetail>(`/contacts/${id}`),
|
|
enabled: !!id,
|
|
});
|
|
}
|
|
|
|
export function useCreateContact() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: Partial<Contact> & { 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<Contact> & { 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<PaginatedResponse<AuditLogEntry>>(`/audit-log?${params.toString()}`),
|
|
retry: false,
|
|
});
|
|
}
|
|
|
|
export function useGlobalSearch(query: string, entityTypes?: string[]) {
|
|
return useQuery({
|
|
queryKey: ['globalSearch', query, entityTypes],
|
|
queryFn: async (): Promise<SearchResult[]> => {
|
|
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<PaginatedResponse<any>>('/notifications'),
|
|
});
|
|
}
|
|
|
|
export function useUnreadNotificationCount() {
|
|
return useQuery({
|
|
queryKey: ['notifications', 'unread-count'],
|
|
queryFn: () => apiGet<number>('/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<any>('/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<any>('/roles');
|
|
return data;
|
|
},
|
|
});
|
|
}
|
|
|
|
export function usePermissions() {
|
|
return useQuery({
|
|
queryKey: ['permissions'],
|
|
queryFn: () => apiGet<PermissionsResponse>('/roles/permissions'),
|
|
});
|
|
}
|
|
|
|
export function useCreateRole() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: { name: string; permissions: Record<string, any>; field_permissions: Record<string, any> }) =>
|
|
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<string, any>; field_permissions?: Record<string, any> } }) =>
|
|
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<string, any> | 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<string, any> | 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,
|
|
) {
|
|
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 useQuery({
|
|
queryKey: ['unifiedContacts', page, pageSize, search, contactType, sortBy, sortOrder],
|
|
queryFn: () =>
|
|
apiGet<PaginatedResponse<UnifiedContact>>(`/contacts?${params.toString()}`),
|
|
});
|
|
}
|
|
|
|
export function useUnifiedContact(id?: string) {
|
|
return useQuery({
|
|
queryKey: ['unifiedContacts', id],
|
|
queryFn: () => apiGet<UnifiedContact & { contact_persons: ContactPerson[] }>(`/contacts/${id}`),
|
|
enabled: !!id,
|
|
});
|
|
}
|
|
|
|
export function useCreateUnifiedContact() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: Partial<UnifiedContact>) => apiPost('/contacts', data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['unifiedContacts'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateUnifiedContact() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: Partial<UnifiedContact> }) =>
|
|
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<ContactPerson> }) =>
|
|
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<ContactPerson> }) =>
|
|
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<PaginatedResponse<UnifiedContact>> {
|
|
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<PaginatedResponse<UnifiedContact>>(`/contacts?${params.toString()}`);
|
|
}
|
|
|
|
export async function fetchContact(id: string): Promise<UnifiedContact & { contact_persons: ContactPerson[] }> {
|
|
return apiGet<UnifiedContact & { contact_persons: ContactPerson[] }>(`/contacts/${id}`);
|
|
}
|
|
|
|
export async function createContact(data: Partial<UnifiedContact>): Promise<UnifiedContact> {
|
|
return apiPost<UnifiedContact>('/contacts', data);
|
|
}
|
|
|
|
export async function updateContact(id: string, data: Partial<UnifiedContact>): Promise<UnifiedContact> {
|
|
return apiPut<UnifiedContact>(`/contacts/${id}`, data);
|
|
}
|
|
|
|
export async function deleteContact(id: string, hard?: boolean): Promise<void> {
|
|
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<ContactPerson>): Promise<ContactPerson> {
|
|
return apiPost<ContactPerson>(`/contacts/${contactId}/persons`, data);
|
|
}
|
|
|
|
export async function updateContactPerson(contactId: string, personId: string, data: Partial<ContactPerson>): Promise<ContactPerson> {
|
|
return apiPut<ContactPerson>(`/contacts/${contactId}/persons/${personId}`, data);
|
|
}
|
|
|
|
export async function deleteContactPerson(contactId: string, personId: string): Promise<void> {
|
|
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<SystemSettings>('/system-settings'),
|
|
});
|
|
}
|
|
|
|
export function useUpdateSystemSettings() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (data: Partial<SystemSettings>) => 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<Currency>) => apiPost('/currencies', data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['currencies'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateCurrency() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: Partial<Currency> }) =>
|
|
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<TaxRate>) => apiPost('/taxes', data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['taxes'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateTax() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: Partial<TaxRate> }) =>
|
|
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<Sequence>) => apiPost('/sequences', data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['sequences'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateSequence() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: Partial<Sequence> }) =>
|
|
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<Address> & { 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<Address> }) =>
|
|
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<string, any>;
|
|
denied_permissions: string[];
|
|
field_permissions: Record<string, any>;
|
|
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<Group>) => apiPost('/groups', data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['groups'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateGroup() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: Partial<Group> }) =>
|
|
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,
|
|
});
|
|
}
|