164 lines
4.7 KiB
TypeScript
164 lines
4.7 KiB
TypeScript
|
|
import { apiFetch, type PaginatedResponse } from './api';
|
||
|
|
|
||
|
|
export interface ContactPersonResponse {
|
||
|
|
id: string;
|
||
|
|
contact_id: string;
|
||
|
|
name: string;
|
||
|
|
function?: string;
|
||
|
|
phone?: string;
|
||
|
|
email?: string;
|
||
|
|
created_at?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ContactResponse {
|
||
|
|
id: string;
|
||
|
|
company_name: string;
|
||
|
|
legal_form?: string;
|
||
|
|
address_street?: string;
|
||
|
|
address_zip?: string;
|
||
|
|
address_city?: string;
|
||
|
|
address_country: string;
|
||
|
|
vat_id?: string;
|
||
|
|
phone?: string;
|
||
|
|
email?: string;
|
||
|
|
website?: string;
|
||
|
|
role: string;
|
||
|
|
vat_id_status: string;
|
||
|
|
is_private: boolean;
|
||
|
|
created_at?: string;
|
||
|
|
updated_at?: string;
|
||
|
|
deleted_at?: string | null;
|
||
|
|
contact_persons: ContactPersonResponse[];
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ContactCreateData {
|
||
|
|
company_name: string;
|
||
|
|
legal_form?: string;
|
||
|
|
address_street?: string;
|
||
|
|
address_zip?: string;
|
||
|
|
address_city?: string;
|
||
|
|
address_country: string;
|
||
|
|
vat_id?: string;
|
||
|
|
phone?: string;
|
||
|
|
email?: string;
|
||
|
|
website?: string;
|
||
|
|
role: string;
|
||
|
|
is_private?: boolean;
|
||
|
|
contact_persons?: ContactPersonCreateData[];
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ContactUpdateData {
|
||
|
|
company_name?: string;
|
||
|
|
legal_form?: string;
|
||
|
|
address_street?: string;
|
||
|
|
address_zip?: string;
|
||
|
|
address_city?: string;
|
||
|
|
address_country?: string;
|
||
|
|
vat_id?: string;
|
||
|
|
phone?: string;
|
||
|
|
email?: string;
|
||
|
|
website?: string;
|
||
|
|
role?: string;
|
||
|
|
is_private?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ContactPersonCreateData {
|
||
|
|
name: string;
|
||
|
|
function?: string;
|
||
|
|
phone?: string;
|
||
|
|
email?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ContactListParams {
|
||
|
|
page?: number;
|
||
|
|
page_size?: number;
|
||
|
|
search?: string;
|
||
|
|
role?: string;
|
||
|
|
is_eu?: boolean;
|
||
|
|
is_private?: boolean;
|
||
|
|
sort?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function listContacts(params: ContactListParams = {}): Promise<PaginatedResponse<ContactResponse>> {
|
||
|
|
const query = new URLSearchParams();
|
||
|
|
if (params.page) query.set('page', String(params.page));
|
||
|
|
if (params.page_size) query.set('page_size', String(params.page_size));
|
||
|
|
if (params.search) query.set('search', params.search);
|
||
|
|
if (params.role) query.set('role', params.role);
|
||
|
|
if (params.is_eu !== undefined) query.set('is_eu', String(params.is_eu));
|
||
|
|
if (params.is_private !== undefined) query.set('is_private', String(params.is_private));
|
||
|
|
if (params.sort) query.set('sort', params.sort);
|
||
|
|
const qs = query.toString();
|
||
|
|
return apiFetch<PaginatedResponse<ContactResponse>>(`/contacts/${qs ? `?${qs}` : ''}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function getContact(id: string): Promise<ContactResponse> {
|
||
|
|
return apiFetch<ContactResponse>(`/contacts/${id}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function createContact(data: ContactCreateData): Promise<ContactResponse> {
|
||
|
|
return apiFetch<ContactResponse>('/contacts/', {
|
||
|
|
method: 'POST',
|
||
|
|
body: JSON.stringify(data),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function updateContact(id: string, data: ContactUpdateData): Promise<ContactResponse> {
|
||
|
|
return apiFetch<ContactResponse>(`/contacts/${id}`, {
|
||
|
|
method: 'PUT',
|
||
|
|
body: JSON.stringify(data),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function deleteContact(id: string): Promise<ContactResponse> {
|
||
|
|
return apiFetch<ContactResponse>(`/contacts/${id}`, { method: 'DELETE' });
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function addContactPerson(contactId: string, data: ContactPersonCreateData): Promise<ContactPersonResponse> {
|
||
|
|
return apiFetch<ContactPersonResponse>(`/contacts/${contactId}/persons`, {
|
||
|
|
method: 'POST',
|
||
|
|
body: JSON.stringify(data),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function removeContactPerson(contactId: string, personId: string): Promise<void> {
|
||
|
|
await apiFetch<void>(`/contacts/${contactId}/persons/${personId}`, { method: 'DELETE' });
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate VAT ID format on the frontend (mirrors backend ust_validation.py).
|
||
|
|
* Returns error message or null if valid.
|
||
|
|
*/
|
||
|
|
export function validateVatIdFormat(vatId: string, countryCode: string): string | null {
|
||
|
|
if (!vatId) return null;
|
||
|
|
const normalized = vatId.trim().toUpperCase().replace(/\s+/g, '');
|
||
|
|
const patterns: Record<string, RegExp> = {
|
||
|
|
DE: /^DE\d{9}$/,
|
||
|
|
AT: /^ATU\d{8}$/,
|
||
|
|
FR: /^FR[A-Za-z0-9]{2}\d{9}$/,
|
||
|
|
NL: /^NL\d{9}B\d{2}$/,
|
||
|
|
PL: /^PL\d{10}$/,
|
||
|
|
CZ: /^CZ\d{8,10}$/,
|
||
|
|
IT: /^IT\d{11}$/,
|
||
|
|
ES: /^ES[A-Za-z0-9]\d{7}[A-Za-z0-9]$/,
|
||
|
|
BE: /^BE\d{10}$/,
|
||
|
|
DK: /^DK\d{8}$/,
|
||
|
|
SE: /^SE\d{10}$/,
|
||
|
|
};
|
||
|
|
const euCountries = new Set([
|
||
|
|
'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR',
|
||
|
|
'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO',
|
||
|
|
'SE', 'SI', 'SK',
|
||
|
|
]);
|
||
|
|
const cc = normalized.substring(0, 2);
|
||
|
|
if (!cc.match(/^[A-Z]{2}$/)) return 'Invalid country code';
|
||
|
|
const pattern = patterns[cc];
|
||
|
|
if (pattern) {
|
||
|
|
return pattern.test(normalized) ? null : `Invalid VAT ID format for ${cc}`;
|
||
|
|
}
|
||
|
|
if (euCountries.has(cc)) {
|
||
|
|
return /^[A-Z]{2}[A-Za-z0-9]{5,15}$/.test(normalized) ? null : `Invalid VAT ID format for ${cc}`;
|
||
|
|
}
|
||
|
|
return `VAT ID validation not supported for country ${cc}`;
|
||
|
|
}
|