feat: add CSV import, activity timeline, and AI insights widget

- CSV Import: modal with file picker, auto column mapping, preview table, import progress
- Activity Timeline: new tab on client detail showing all communications, events, status changes
- AI Insights Widget: dashboard card showing stale clients, upcoming birthdays, suggested follow-ups
- Import button on Clients page header
This commit is contained in:
2026-01-29 12:43:30 +00:00
parent 0b7bddb81c
commit e7c2e396c0
6 changed files with 602 additions and 16 deletions

View File

@@ -1,4 +1,4 @@
import type { Profile, Client, ClientCreate, Event, EventCreate, Email, EmailGenerate, User, Invite } from '@/types';
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'
@@ -157,6 +157,54 @@ class ApiClient {
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();