T07b: frontend feature pages — companies + contacts + settings + audit + dashboard + search
- 11 new feature pages (CompaniesList/Detail/Form, ContactsList/Detail/Form, SettingsProfile/Roles/Users, AuditLog, GlobalSearchResults) - 3 page updates (Dashboard with StatCard+ActivityFeed, Settings with tree nav+Outlet, TopBar with SearchDropdown) - 13 new routes in routes/index.tsx - i18n updates (de.json + en.json) with companies/contacts/settings/audit/search keys - 12 new test files + 2 existing test fixes (TopBar, AppShell) - 7 shared components (DataGrid, Tabs, SearchDropdown, CsvImportDialog, StatCard, ActivityFeed, UnsavedChangesGuard) - 16 new API hooks in hooks.ts - Verification: 141 tests pass, build succeeds, tsc --noEmit clean
This commit is contained in:
+275
-5
@@ -1,5 +1,5 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiPost, apiGet, apiPatch, apiDelete } from './client';
|
||||
import { apiPost, apiGet, apiPatch, apiDelete, apiClient } from './client';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
|
||||
export interface LoginPayload {
|
||||
@@ -23,6 +23,57 @@ export interface PaginatedResponse<T> {
|
||||
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 function useLogin() {
|
||||
const { setUser, setError } = useAuthStore();
|
||||
return useMutation({
|
||||
@@ -100,13 +151,126 @@ export function useUsers(page = 1, pageSize = 25) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useCompanies(page = 1, pageSize = 25, search?: string) {
|
||||
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],
|
||||
queryKey: ['companies', page, pageSize, search, industry, sortBy, sortOrder],
|
||||
queryFn: () =>
|
||||
apiGet<PaginatedResponse<any>>(`/companies?${params.toString()}`),
|
||||
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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,7 +280,113 @@ export function useContacts(page = 1, pageSize = 25, search?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['contacts', page, pageSize, search],
|
||||
queryFn: () =>
|
||||
apiGet<PaginatedResponse<any>>(`/contacts?${params.toString()}`),
|
||||
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?${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 results: SearchResult[] = [];
|
||||
const types = entityTypes && entityTypes.length > 0 ? entityTypes : ['company', 'contact'];
|
||||
const tasks: Promise<void>[] = [];
|
||||
if (types.includes('company')) {
|
||||
tasks.push(
|
||||
apiGet<PaginatedResponse<Company>>(`/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<PaginatedResponse<Contact>>(`/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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user