feat(companies): UI fuer Firmen-Verwaltung mit Ansprechpartner-Links — Modul 12/16 des UI-Backlogs
Check Cross-Plugin Imports / check (push) Has been cancelled

This commit is contained in:
Agent Zero
2026-09-15 22:56:38 +02:00
parent 71ffee021e
commit 06b72843da
8 changed files with 1099 additions and 4 deletions
+173
View File
@@ -0,0 +1,173 @@
/**
* Companies API client — CRUD, search, export and contact links for
* companies (UI-Backlog module 12/16).
*
* Backend: /api/v1/companies (contacts plugin). Companies are Contact
* entities with type='company'; industry and description live in the
* custom JSONB field.
* - GET / → paginated list (search, industry, sort)
* - POST / → create (201)
* - GET /export → CSV/XLSX download
* - GET /{id} → detail incl. linked contact persons
* - PUT /{id} → update
* - DELETE /{id} → soft-delete (?cascade=true: persons too)
* - POST /{id}/contacts/{cid} → link a contact person
* - DELETE /{id}/contacts/{cid} → unlink (removes all persons links)
* - GET /{id}/emails → emails mentioning the company
* Permissions: contacts:read (list/get/export) / contacts:write
* (create/update/link/unlink) / contacts:delete (delete).
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiGet, apiPost, apiPut, apiDelete } from '@/api/client';
export interface Company {
id: string;
name: string;
displayname: string | null;
status: string | null;
industry: string | null;
description: string | null;
email_1: string | null;
email_2: string | null;
phone_1: string | null;
phone_2: string | null;
website: string | null;
mailing_city: string | null;
mailing_postalcode: string | null;
mailing_country: string | null;
tags: string | null;
custom: Record<string, unknown> | null;
/** Only present in the detail response: */
contacts?: CompanyContactPerson[];
}
export interface CompanyContactPerson {
id: string;
firstname: string | null;
lastname: string | null;
email: string | null;
phone: string | null;
}
export interface CompanyListResult {
items: Company[];
total: number;
page: number;
page_size: number;
}
export interface CompanyListParams {
page?: number;
page_size?: number;
search?: string | null;
industry?: string | null;
sort_by?: string;
sort_order?: 'asc' | 'desc';
}
export interface CompanyCreatePayload {
name: string;
status?: string;
industry?: string;
description?: string;
email_1?: string;
phone_1?: string;
website?: string;
mailing_city?: string;
mailing_country?: string;
[key: string]: unknown;
}
export type CompanyUpdatePayload = Partial<CompanyCreatePayload>;
// ─── Query hooks ─────────────────────────────────────────────
export function useCompanies(params: CompanyListParams = {}) {
const searchParams = new URLSearchParams();
if (params.page) searchParams.set('page', String(params.page));
if (params.page_size) searchParams.set('page_size', String(params.page_size));
if (params.search) searchParams.set('search', params.search);
if (params.industry) searchParams.set('industry', params.industry);
if (params.sort_by) searchParams.set('sort_by', params.sort_by);
if (params.sort_order) searchParams.set('sort_order', params.sort_order);
const qs = searchParams.toString();
return useQuery<CompanyListResult>({
queryKey: ['companies', params.page, params.page_size, params.search, params.industry, params.sort_by, params.sort_order],
queryFn: () => apiGet<CompanyListResult>(`/companies${qs ? `?${qs}` : ''}`),
});
}
export function useCompany(companyId: string | null) {
return useQuery<Company>({
queryKey: ['companies', 'detail', companyId],
queryFn: () => apiGet<Company>(`/companies/${companyId}`),
enabled: !!companyId,
});
}
export function useCompanyEmails(companyId: string | null) {
return useQuery<unknown[]>({
queryKey: ['companies', 'emails', companyId],
queryFn: () => apiGet<unknown[]>(`/companies/${companyId}/emails`),
enabled: !!companyId,
});
}
// ─── Mutation hooks ──────────────────────────────────────────
function invalidateCompanies(qc: ReturnType<typeof useQueryClient>) {
qc.invalidateQueries({ queryKey: ['companies'] });
}
export function useCreateCompany() {
const qc = useQueryClient();
return useMutation<Company, Error, CompanyCreatePayload>({
mutationFn: (data) => apiPost<Company>('/companies', data),
onSuccess: () => invalidateCompanies(qc),
});
}
export function useUpdateCompany() {
const qc = useQueryClient();
return useMutation<Company, Error, { id: string; data: CompanyUpdatePayload }>({
mutationFn: ({ id, data }) => apiPut<Company>(`/companies/${id}`, data),
onSuccess: (company) => {
invalidateCompanies(qc);
qc.invalidateQueries({ queryKey: ['companies', 'detail', company.id] });
},
});
}
export function useDeleteCompany() {
const qc = useQueryClient();
return useMutation<void, Error, { id: string; cascade?: boolean }>({
mutationFn: ({ id, cascade }) =>
apiDelete(`/companies/${id}${cascade ? '?cascade=true' : ''}`),
onSuccess: () => invalidateCompanies(qc),
});
}
export function useLinkContact() {
const qc = useQueryClient();
return useMutation<{ company_id: string; contact_id: string }, Error, { companyId: string; contactId: string }>({
mutationFn: ({ companyId, contactId }) =>
apiPost(`/companies/${companyId}/contacts/${contactId}`),
onSuccess: (r) => {
invalidateCompanies(qc);
qc.invalidateQueries({ queryKey: ['companies', 'detail', r.company_id] });
},
});
}
export function useUnlinkContact() {
const qc = useQueryClient();
return useMutation<void, Error, { companyId: string; contactId: string }>({
mutationFn: ({ companyId, contactId }) =>
apiDelete(`/companies/${companyId}/contacts/${contactId}`),
onSuccess: (_, vars) => {
invalidateCompanies(qc);
qc.invalidateQueries({ queryKey: ['companies', 'detail', vars.companyId] });
},
});
}