544 lines
18 KiB
TypeScript
544 lines
18 KiB
TypeScript
import type { Profile, Client, ClientCreate, ClientNote, Event, EventCreate, Email, EmailGenerate, User, Invite, ActivityItem, InsightsData, ImportPreview, ImportResult, NetworkMatch, NetworkStats, Notification, Interaction, BulkEmailResult } from '@/types';
|
|
|
|
const API_BASE = import.meta.env.PROD
|
|
? 'https://api.thenetwork.donovankelly.xyz/api'
|
|
: '/api';
|
|
|
|
const AUTH_BASE = import.meta.env.PROD
|
|
? 'https://api.thenetwork.donovankelly.xyz'
|
|
: '';
|
|
|
|
const TOKEN_KEY = 'network-auth-token';
|
|
|
|
class ApiClient {
|
|
private getToken(): string | null {
|
|
return localStorage.getItem(TOKEN_KEY);
|
|
}
|
|
|
|
setToken(token: string | null) {
|
|
if (token) {
|
|
localStorage.setItem(TOKEN_KEY, token);
|
|
} else {
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
}
|
|
}
|
|
|
|
private authHeaders(): HeadersInit {
|
|
const token = this.getToken();
|
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
}
|
|
|
|
private async fetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
const headers: HeadersInit = {
|
|
'Content-Type': 'application/json',
|
|
...this.authHeaders(),
|
|
...options.headers,
|
|
};
|
|
|
|
const response = await fetch(`${API_BASE}${path}`, {
|
|
...options,
|
|
headers,
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
throw new Error(error.error || error.message || 'Request failed');
|
|
}
|
|
|
|
const text = await response.text();
|
|
if (!text) return {} as T;
|
|
return JSON.parse(text);
|
|
}
|
|
|
|
// Auth
|
|
async login(email: string, password: string) {
|
|
const response = await fetch(`${AUTH_BASE}/api/auth/sign-in/email`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password }),
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ message: 'Login failed' }));
|
|
throw new Error(error.message || 'Login failed');
|
|
}
|
|
// Capture bearer token from response header
|
|
const authToken = response.headers.get('set-auth-token');
|
|
if (authToken) {
|
|
this.setToken(authToken);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async logout() {
|
|
await fetch(`${AUTH_BASE}/api/auth/sign-out`, {
|
|
method: 'POST',
|
|
headers: this.authHeaders(),
|
|
credentials: 'include',
|
|
});
|
|
this.setToken(null);
|
|
}
|
|
|
|
async getSession(): Promise<{ user: User } | null> {
|
|
try {
|
|
const response = await fetch(`${AUTH_BASE}/api/auth/get-session`, {
|
|
headers: this.authHeaders(),
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) return null;
|
|
const data = await response.json();
|
|
if (!data || !data.user) return null;
|
|
return data;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Account (email & password changes via profile API)
|
|
async changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
|
return this.fetch('/profile/password', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ currentPassword, newPassword }),
|
|
});
|
|
}
|
|
|
|
async changeEmail(newEmail: string): Promise<void> {
|
|
return this.fetch('/profile/email', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ newEmail }),
|
|
});
|
|
}
|
|
|
|
// Profile
|
|
async getProfile(): Promise<Profile> {
|
|
return this.fetch('/profile');
|
|
}
|
|
|
|
async updateProfile(data: Partial<Profile>): Promise<Profile> {
|
|
return this.fetch('/profile', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
// Clients
|
|
async getClients(params?: { search?: string; tag?: string }): Promise<Client[]> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.search) searchParams.set('search', params.search);
|
|
if (params?.tag) searchParams.set('tag', params.tag);
|
|
const query = searchParams.toString();
|
|
return this.fetch(`/clients${query ? `?${query}` : ''}`);
|
|
}
|
|
|
|
async getClient(id: string): Promise<Client> {
|
|
return this.fetch(`/clients/${id}`);
|
|
}
|
|
|
|
async createClient(data: ClientCreate): Promise<Client> {
|
|
return this.fetch('/clients', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateClient(id: string, data: Partial<ClientCreate>): Promise<Client> {
|
|
return this.fetch(`/clients/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteClient(id: string): Promise<void> {
|
|
await this.fetch(`/clients/${id}`, { method: 'DELETE' });
|
|
}
|
|
|
|
async markContacted(id: string): Promise<Client> {
|
|
return this.fetch(`/clients/${id}/contacted`, { method: 'POST' });
|
|
}
|
|
|
|
// CSV Import
|
|
async importPreview(file: File): Promise<ImportPreview> {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const token = this.getToken();
|
|
const headers: HeadersInit = token ? { Authorization: `Bearer ${token}` } : {};
|
|
const response = await fetch(`${API_BASE}/clients/import/preview`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: formData,
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ error: 'Preview failed' }));
|
|
throw new Error(error.error || 'Preview failed');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async importClients(file: File, mapping: Record<number, string>): Promise<ImportResult> {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('mapping', JSON.stringify(mapping));
|
|
const token = this.getToken();
|
|
const headers: HeadersInit = token ? { Authorization: `Bearer ${token}` } : {};
|
|
const response = await fetch(`${API_BASE}/clients/import`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: formData,
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ error: 'Import failed' }));
|
|
throw new Error(error.error || 'Import failed');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
// Activity Timeline
|
|
async getClientActivity(clientId: string): Promise<ActivityItem[]> {
|
|
return this.fetch(`/clients/${clientId}/activity`);
|
|
}
|
|
|
|
// Insights
|
|
async getInsights(): Promise<InsightsData> {
|
|
return this.fetch('/insights');
|
|
}
|
|
|
|
// Events
|
|
async getEvents(params?: { clientId?: string; type?: string; upcoming?: number }): Promise<Event[]> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.clientId) searchParams.set('clientId', params.clientId);
|
|
if (params?.type) searchParams.set('type', params.type);
|
|
if (params?.upcoming) searchParams.set('upcoming', String(params.upcoming));
|
|
const query = searchParams.toString();
|
|
return this.fetch(`/events${query ? `?${query}` : ''}`);
|
|
}
|
|
|
|
async getEvent(id: string): Promise<Event> {
|
|
return this.fetch(`/events/${id}`);
|
|
}
|
|
|
|
async createEvent(data: EventCreate): Promise<Event> {
|
|
return this.fetch('/events', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateEvent(id: string, data: Partial<EventCreate>): Promise<Event> {
|
|
return this.fetch(`/events/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteEvent(id: string): Promise<void> {
|
|
await this.fetch(`/events/${id}`, { method: 'DELETE' });
|
|
}
|
|
|
|
async syncAllEvents(): Promise<void> {
|
|
await this.fetch('/events/sync-all', { method: 'POST' });
|
|
}
|
|
|
|
async syncClientEvents(clientId: string): Promise<void> {
|
|
await this.fetch(`/events/sync/${clientId}`, { method: 'POST' });
|
|
}
|
|
|
|
// Emails
|
|
async getEmails(params?: { status?: string; clientId?: string }): Promise<Email[]> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.status) searchParams.set('status', params.status);
|
|
if (params?.clientId) searchParams.set('clientId', params.clientId);
|
|
const query = searchParams.toString();
|
|
return this.fetch(`/emails${query ? `?${query}` : ''}`);
|
|
}
|
|
|
|
async getEmail(id: string): Promise<Email> {
|
|
return this.fetch(`/emails/${id}`);
|
|
}
|
|
|
|
async generateEmail(data: EmailGenerate): Promise<Email> {
|
|
return this.fetch('/emails/generate', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async generateBirthdayEmail(clientId: string, provider?: string): Promise<Email> {
|
|
return this.fetch('/emails/generate-birthday', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ clientId, provider }),
|
|
});
|
|
}
|
|
|
|
async updateEmail(id: string, data: { subject?: string; content?: string }): Promise<Email> {
|
|
return this.fetch(`/emails/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async sendEmail(id: string): Promise<Email> {
|
|
return this.fetch(`/emails/${id}/send`, { method: 'POST' });
|
|
}
|
|
|
|
async deleteEmail(id: string): Promise<void> {
|
|
await this.fetch(`/emails/${id}`, { method: 'DELETE' });
|
|
}
|
|
|
|
// Network Matching
|
|
async getNetworkMatches(params?: { minScore?: number; limit?: number }): Promise<{ matches: NetworkMatch[]; total: number; clientCount: number }> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.minScore) searchParams.set('minScore', String(params.minScore));
|
|
if (params?.limit) searchParams.set('limit', String(params.limit));
|
|
const query = searchParams.toString();
|
|
return this.fetch(`/network/matches${query ? `?${query}` : ''}`);
|
|
}
|
|
|
|
async getClientMatches(clientId: string, minScore?: number): Promise<{ matches: NetworkMatch[]; client: { id: string; name: string } }> {
|
|
const query = minScore ? `?minScore=${minScore}` : '';
|
|
return this.fetch(`/network/matches/${clientId}${query}`);
|
|
}
|
|
|
|
async generateIntro(clientAId: string, clientBId: string, reasons: string[], provider?: string): Promise<{ introSuggestion: string }> {
|
|
return this.fetch('/network/intro', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ clientAId, clientBId, reasons, provider }),
|
|
});
|
|
}
|
|
|
|
async getNetworkStats(): Promise<NetworkStats> {
|
|
return this.fetch('/network/stats');
|
|
}
|
|
|
|
// Admin
|
|
async getUsers(): Promise<User[]> {
|
|
return this.fetch('/admin/users');
|
|
}
|
|
|
|
async updateUserRole(userId: string, role: string): Promise<void> {
|
|
await this.fetch(`/admin/users/${userId}/role`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ role }),
|
|
});
|
|
}
|
|
|
|
async deleteUser(userId: string): Promise<void> {
|
|
await this.fetch(`/admin/users/${userId}`, { method: 'DELETE' });
|
|
}
|
|
|
|
async createInvite(data: { email: string; name: string; role?: string }): Promise<Invite & { setupUrl: string }> {
|
|
return this.fetch('/admin/invites', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async getInvites(): Promise<Invite[]> {
|
|
return this.fetch('/admin/invites');
|
|
}
|
|
|
|
async deleteInvite(id: string): Promise<void> {
|
|
await this.fetch(`/admin/invites/${id}`, { method: 'DELETE' });
|
|
}
|
|
|
|
async createPasswordReset(userId: string): Promise<{ resetUrl: string; email: string }> {
|
|
return this.fetch(`/admin/users/${userId}/reset-password`, { method: 'POST' });
|
|
}
|
|
|
|
// Invite acceptance (public - no auth)
|
|
async validateInvite(token: string): Promise<{ id: string; email: string; name: string; role: string; expiresAt: string }> {
|
|
const response = await fetch(`${AUTH_BASE}/auth/invite/${token}`);
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ error: 'Invalid invite' }));
|
|
throw new Error(error.error || 'Invalid invite');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async requestPasswordReset(email: string): Promise<{ success: boolean; message: string; resetUrl?: string }> {
|
|
const response = await fetch(`${AUTH_BASE}/auth/reset-password/request`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email }),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ error: 'Request failed' }));
|
|
throw new Error(error.error || 'Request failed');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async validateResetToken(token: string): Promise<{ valid: boolean; email?: string }> {
|
|
const response = await fetch(`${AUTH_BASE}/auth/reset-password/${token}`);
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ error: 'Invalid token' }));
|
|
throw new Error(error.error || 'Invalid or expired reset link');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async resetPassword(token: string, password: string): Promise<{ success: boolean }> {
|
|
const response = await fetch(`${AUTH_BASE}/auth/reset-password/${token}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ password }),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ error: 'Reset failed' }));
|
|
throw new Error(error.error || 'Failed to reset password');
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async acceptInvite(token: string, password: string, name?: string): Promise<{ success: boolean; token?: string }> {
|
|
const response = await fetch(`${AUTH_BASE}/auth/invite/${token}/accept`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ password, name }),
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ error: 'Failed to accept invite' }));
|
|
throw new Error(error.error || 'Failed to accept invite');
|
|
}
|
|
return response.json();
|
|
}
|
|
// Client Notes
|
|
async getClientNotes(clientId: string): Promise<ClientNote[]> {
|
|
return this.fetch(`/clients/${clientId}/notes`);
|
|
}
|
|
|
|
async createClientNote(clientId: string, content: string): Promise<ClientNote> {
|
|
return this.fetch(`/clients/${clientId}/notes`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ content }),
|
|
});
|
|
}
|
|
|
|
async updateClientNote(clientId: string, noteId: string, data: { content?: string; pinned?: boolean }): Promise<ClientNote> {
|
|
return this.fetch(`/clients/${clientId}/notes/${noteId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteClientNote(clientId: string, noteId: string): Promise<void> {
|
|
await this.fetch(`/clients/${clientId}/notes/${noteId}`, { method: 'DELETE' });
|
|
}
|
|
|
|
// Reports & Analytics
|
|
async getReportsOverview(): Promise<any> {
|
|
return this.fetch('/reports/overview');
|
|
}
|
|
|
|
async getReportsGrowth(): Promise<any> {
|
|
return this.fetch('/reports/growth');
|
|
}
|
|
|
|
async getReportsIndustries(): Promise<any[]> {
|
|
return this.fetch('/reports/industries');
|
|
}
|
|
|
|
async getReportsTags(): Promise<any[]> {
|
|
return this.fetch('/reports/tags');
|
|
}
|
|
|
|
async getReportsEngagement(): Promise<any> {
|
|
return this.fetch('/reports/engagement');
|
|
}
|
|
|
|
async getNotificationsLegacy(): Promise<any> {
|
|
return this.fetch('/reports/notifications');
|
|
}
|
|
|
|
// Real notifications (from notifications table)
|
|
async getNotifications(params?: { unreadOnly?: boolean; limit?: number }): Promise<{ notifications: Notification[]; unreadCount: number }> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.unreadOnly) searchParams.set('unreadOnly', 'true');
|
|
if (params?.limit) searchParams.set('limit', String(params.limit));
|
|
const query = searchParams.toString();
|
|
return this.fetch(`/notifications${query ? `?${query}` : ''}`);
|
|
}
|
|
|
|
async markNotificationRead(id: string): Promise<Notification> {
|
|
return this.fetch(`/notifications/${id}/read`, { method: 'PUT' });
|
|
}
|
|
|
|
async markAllNotificationsRead(): Promise<void> {
|
|
await this.fetch('/notifications/mark-all-read', { method: 'POST' });
|
|
}
|
|
|
|
async deleteNotification(id: string): Promise<void> {
|
|
await this.fetch(`/notifications/${id}`, { method: 'DELETE' });
|
|
}
|
|
|
|
// Interactions
|
|
async getClientInteractions(clientId: string): Promise<Interaction[]> {
|
|
return this.fetch(`/clients/${clientId}/interactions`);
|
|
}
|
|
|
|
async createInteraction(clientId: string, data: {
|
|
type: string; title: string; description?: string; duration?: number; contactedAt: string;
|
|
}): Promise<Interaction> {
|
|
return this.fetch(`/clients/${clientId}/interactions`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateInteraction(id: string, data: Partial<{
|
|
type: string; title: string; description?: string; duration?: number; contactedAt: string;
|
|
}>): Promise<Interaction> {
|
|
return this.fetch(`/interactions/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteInteraction(id: string): Promise<void> {
|
|
await this.fetch(`/interactions/${id}`, { method: 'DELETE' });
|
|
}
|
|
|
|
async getRecentInteractions(limit?: number): Promise<Interaction[]> {
|
|
const query = limit ? `?limit=${limit}` : '';
|
|
return this.fetch(`/interactions/recent${query}`);
|
|
}
|
|
|
|
// Bulk Email
|
|
async bulkGenerateEmails(clientIds: string[], purpose: string, provider?: 'anthropic' | 'openai'): Promise<BulkEmailResult> {
|
|
return this.fetch('/emails/bulk-generate', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ clientIds, purpose, provider }),
|
|
});
|
|
}
|
|
|
|
async bulkSendEmails(batchId: string): Promise<{ batchId: string; total: number; sent: number; failed: number }> {
|
|
return this.fetch('/emails/bulk-send', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ batchId }),
|
|
});
|
|
}
|
|
|
|
async exportClientsCSV(): Promise<void> {
|
|
const token = this.getToken();
|
|
const headers: HeadersInit = token ? { Authorization: `Bearer ${token}` } : {};
|
|
const response = await fetch(`${API_BASE}/reports/export/clients`, {
|
|
headers,
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) throw new Error('Export failed');
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `clients-export-${new Date().toISOString().split('T')[0]}.csv`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
window.URL.revokeObjectURL(url);
|
|
}
|
|
}
|
|
|
|
export const api = new ApiClient();
|