mono/packages/ui/src/modules/contacts/client-contacts.ts
2026-03-21 20:18:25 +01:00

222 lines
8.2 KiB
TypeScript

import { supabase as defaultSupabase } from "@/integrations/supabase/client";
import { fetchWithDeduplication } from "@/lib/db";
// ─── Types ────────────────────────────────────────────────────────────────────
export interface ContactEmail {
email: string;
label?: string;
primary?: boolean;
}
export interface ContactAddress {
street?: string;
city?: string;
state?: string;
postal_code?: string;
country?: string;
label?: string;
}
export interface Contact {
id: string;
owner_id: string;
name?: string | null;
first_name?: string | null;
last_name?: string | null;
emails: ContactEmail[];
phone?: string | null;
address: ContactAddress[];
source?: string | null;
language?: string | null;
status?: 'active' | 'unsubscribed' | 'bounced' | 'blocked';
organization?: string | null;
title?: string | null;
notes?: string | null;
tags?: string[] | null;
log?: any;
meta?: Record<string, any>;
created_at?: string;
updated_at?: string;
}
export interface ContactGroup {
id: string;
owner_id: string;
name: string;
description?: string | null;
meta?: Record<string, any>;
created_at?: string;
updated_at?: string;
}
// ─── Auth helper ──────────────────────────────────────────────────────────────
async function authHeaders(contentType?: string): Promise<HeadersInit> {
const { data } = await defaultSupabase.auth.getSession();
const token = data.session?.access_token;
const h: HeadersInit = {};
if (token) h['Authorization'] = `Bearer ${token}`;
if (contentType) h['Content-Type'] = contentType;
return h;
}
const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
async function apiFetch(path: string, init?: RequestInit) {
const url = SERVER_URL ? `${SERVER_URL}${path}` : path;
const res = await fetch(url, init);
if (!res.ok) {
const err = await res.text().catch(() => res.statusText);
throw new Error(`${path}${res.status}: ${err}`);
}
const ct = res.headers.get('content-type') || '';
return ct.includes('json') ? res.json() : res.text();
}
// ─── Contacts CRUD ────────────────────────────────────────────────────────────
export const fetchContacts = async (options?: {
group?: string;
q?: string;
status?: string;
limit?: number;
offset?: number;
}): Promise<Contact[]> => {
const params = new URLSearchParams();
if (options?.group) params.set('group', options.group);
if (options?.q) params.set('q', options.q);
if (options?.status) params.set('status', options.status);
if (options?.limit != null) params.set('limit', String(options.limit));
if (options?.offset != null) params.set('offset', String(options.offset));
return fetchWithDeduplication(`contacts-list-${params.toString()}`, async () => {
const headers = await authHeaders();
return apiFetch(`/api/contacts?${params}`, { headers });
});
};
export const createContact = async (data: Partial<Contact>): Promise<Contact> => {
return apiFetch('/api/contacts', {
method: 'POST',
headers: await authHeaders('application/json'),
body: JSON.stringify(data),
});
};
export const getContact = async (id: string): Promise<Contact> => {
return fetchWithDeduplication(`contact-${id}`, async () => {
const headers = await authHeaders();
return apiFetch(`/api/contacts/${id}`, { headers });
});
};
export const updateContact = async (id: string, data: Partial<Contact>): Promise<Contact> => {
return apiFetch(`/api/contacts/${id}`, {
method: 'PATCH',
headers: await authHeaders('application/json'),
body: JSON.stringify(data),
});
};
export const deleteContact = async (id: string): Promise<void> => {
await apiFetch(`/api/contacts/${id}`, {
method: 'DELETE',
headers: await authHeaders(),
});
};
export const batchDeleteContacts = async (ids: string[]): Promise<{ deleted: number }> => {
return apiFetch('/api/contacts/batch-delete', {
method: 'POST',
headers: await authHeaders('application/json'),
body: JSON.stringify({ ids }),
});
};
// ─── Import / Export ──────────────────────────────────────────────────────────
export const importContacts = async (
body: Contact[] | string,
format: 'json' | 'vcard' = 'json',
groupId?: string
): Promise<{ imported: number; skipped: number }> => {
const isJson = format === 'json';
const qs = new URLSearchParams();
qs.set('format', format);
if (groupId) qs.set('group_id', groupId);
return apiFetch(`/api/contacts/import?${qs.toString()}`, {
method: 'POST',
headers: await authHeaders(isJson ? 'application/json' : 'text/vcard'),
body: isJson ? JSON.stringify(body) : (body as string),
});
};
export const exportContacts = async (options?: {
format?: 'json' | 'vcard';
group?: string;
ids?: string[];
}): Promise<string | Contact[]> => {
const params = new URLSearchParams();
if (options?.format) params.set('format', options.format);
if (options?.group) params.set('group', options.group);
if (options?.ids?.length) params.set('ids', options.ids.join(','));
return apiFetch(`/api/contacts/export?${params}`, { headers: await authHeaders() });
};
// ─── Groups ───────────────────────────────────────────────────────────────────
export const fetchContactGroups = async (): Promise<ContactGroup[]> => {
return fetchWithDeduplication('contact-groups', async () => {
const headers = await authHeaders();
return apiFetch('/api/contact-groups', { headers });
});
};
export const createContactGroup = async (data: Pick<ContactGroup, 'name'> & Partial<ContactGroup>): Promise<ContactGroup> => {
return apiFetch('/api/contact-groups', {
method: 'POST',
headers: await authHeaders('application/json'),
body: JSON.stringify(data),
});
};
export const updateContactGroup = async (id: string, data: Partial<ContactGroup>): Promise<ContactGroup> => {
return apiFetch(`/api/contact-groups/${id}`, {
method: 'PATCH',
headers: await authHeaders('application/json'),
body: JSON.stringify(data),
});
};
export const deleteContactGroup = async (id: string): Promise<void> => {
await apiFetch(`/api/contact-groups/${id}`, {
method: 'DELETE',
headers: await authHeaders(),
});
};
export const addGroupMembers = async (groupId: string, contactIds: string[]): Promise<{ added: number }> => {
return apiFetch(`/api/contact-groups/${groupId}/members`, {
method: 'POST',
headers: await authHeaders('application/json'),
body: JSON.stringify({ contact_ids: contactIds }),
});
};
/** Returns all group memberships for the current user's contacts as {contact_id, group_id} rows. */
export const fetchGroupMembers = async (): Promise<{ contact_id: string; group_id: string }[]> => {
return fetchWithDeduplication('contact-group-members', async () => {
const headers = await authHeaders();
return apiFetch('/api/contact-groups/members', { headers });
});
};
export const removeGroupMember = async (groupId: string, contactId: string): Promise<void> => {
await apiFetch(`/api/contact-groups/${groupId}/members/${contactId}`, {
method: 'DELETE',
headers: await authHeaders(),
});
};