Files
network-app-web/src/lib/api.ts
Hammer 4c7a8be5b8 feat: Reports & Analytics page, CSV export, notification bell in header
- Reports page with overview stats, client growth chart, email activity chart
- Engagement breakdown (engaged/warm/cooling/cold) with stacked bar
- Industry and tag distribution charts
- At-risk client lists (cold + cooling)
- CSV export button downloads all clients
- Notification bell in top bar: overdue events, upcoming events, stale clients, pending drafts
- Dismissable notifications with priority indicators
- Added Reports to sidebar nav between Network and Settings
2026-01-29 13:04:53 +00:00

453 lines
14 KiB
TypeScript

import type { Profile, Client, ClientCreate, Event, EventCreate, Email, EmailGenerate, User, Invite, ActivityItem, InsightsData, ImportPreview, ImportResult, NetworkMatch, NetworkStats } 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();
}
// 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 getNotifications(): Promise<any> {
return this.fetch('/reports/notifications');
}
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();