Frontend: unified 3-column contacts page (folder tree | list/table/cards | detail), all Rentman fields, ContactPerson CRUD, removed old Contact/Company pages

This commit is contained in:
Agent Zero
2026-07-19 21:30:26 +02:00
parent cf75680583
commit 3177daf47f
25 changed files with 1869 additions and 1592 deletions
+265 -1
View File
@@ -1,5 +1,5 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiPost, apiGet, apiPatch, apiDelete, apiClient, setCsrfToken } from './client';
import { apiPost, apiGet, apiPatch, apiPut, apiDelete, apiClient, setCsrfToken } from './client';
import { useAuthStore } from '@/store/authStore';
export interface LoginPayload {
@@ -567,6 +567,270 @@ export function useDeleteRole() {
});
}
// ════════════════════════════════════════════════════════════════════════════
// ════════════════════════════════════════════════════════════════════════════
// 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
// ════════════════════════════════════════════════════════════════════════════