= {
+ lead: 'gray',
+ prospect: 'blue',
+ onboarding: 'yellow',
+ active: 'green',
+ inactive: 'red',
+};
+
+export function StageBadge({ stage, onClick }: { stage?: string; onClick?: () => void }) {
+ const s = stage || 'lead';
+ return {stageLabels[s] || s};
+}
diff --git a/src/components/ClientForm.tsx b/src/components/ClientForm.tsx
index 11d60d3..d59e31d 100644
--- a/src/components/ClientForm.tsx
+++ b/src/components/ClientForm.tsx
@@ -28,6 +28,7 @@ export default function ClientForm({ initialData, onSubmit, loading }: ClientFor
family: initialData?.family || { spouse: '', children: [] },
notes: initialData?.notes || '',
tags: initialData?.tags || [],
+ stage: initialData?.stage || 'lead',
});
const [tagInput, setTagInput] = useState('');
@@ -109,6 +110,18 @@ export default function ClientForm({ initialData, onSubmit, loading }: ClientFor
+ {/* Stage */}
+
+
+
+
+
{/* Address */}
diff --git a/src/components/ClientNotes.tsx b/src/components/ClientNotes.tsx
new file mode 100644
index 0000000..613c348
--- /dev/null
+++ b/src/components/ClientNotes.tsx
@@ -0,0 +1,214 @@
+import { useEffect, useState, useRef } from 'react';
+import { api } from '@/lib/api';
+import type { ClientNote } from '@/types';
+import { Pin, Trash2, Edit3, Check, X, Plus, StickyNote } from 'lucide-react';
+import { cn, getRelativeTime } from '@/lib/utils';
+
+interface ClientNotesProps {
+ clientId: string;
+}
+
+export default function ClientNotes({ clientId }: ClientNotesProps) {
+ const [notes, setNotes] = useState
([]);
+ const [loading, setLoading] = useState(true);
+ const [newNote, setNewNote] = useState('');
+ const [adding, setAdding] = useState(false);
+ const [editingId, setEditingId] = useState(null);
+ const [editContent, setEditContent] = useState('');
+ const textareaRef = useRef(null);
+
+ const fetchNotes = async () => {
+ try {
+ const data = await api.getClientNotes(clientId);
+ setNotes(data);
+ } catch {
+ // ignore
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchNotes();
+ }, [clientId]);
+
+ const handleAdd = async () => {
+ if (!newNote.trim()) return;
+ setAdding(true);
+ try {
+ const note = await api.createClientNote(clientId, newNote.trim());
+ setNotes(prev => [note, ...prev]);
+ setNewNote('');
+ } catch {
+ // ignore
+ } finally {
+ setAdding(false);
+ }
+ };
+
+ const handleDelete = async (noteId: string) => {
+ if (!confirm('Delete this note?')) return;
+ try {
+ await api.deleteClientNote(clientId, noteId);
+ setNotes(prev => prev.filter(n => n.id !== noteId));
+ } catch {
+ // ignore
+ }
+ };
+
+ const handleTogglePin = async (note: ClientNote) => {
+ try {
+ const updated = await api.updateClientNote(clientId, note.id, { pinned: !note.pinned });
+ setNotes(prev => prev.map(n => n.id === note.id ? updated : n)
+ .sort((a, b) => {
+ if (a.pinned && !b.pinned) return -1;
+ if (!a.pinned && b.pinned) return 1;
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
+ }));
+ } catch {
+ // ignore
+ }
+ };
+
+ const handleStartEdit = (note: ClientNote) => {
+ setEditingId(note.id);
+ setEditContent(note.content);
+ };
+
+ const handleSaveEdit = async (noteId: string) => {
+ if (!editContent.trim()) return;
+ try {
+ const updated = await api.updateClientNote(clientId, noteId, { content: editContent.trim() });
+ setNotes(prev => prev.map(n => n.id === noteId ? updated : n));
+ setEditingId(null);
+ } catch {
+ // ignore
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* New note input */}
+
+
+ {/* Notes list */}
+ {notes.length === 0 ? (
+
+
+
No notes yet. Add your first note above.
+
+ ) : (
+
+ {notes.map(note => (
+
+ {editingId === note.id ? (
+
+ ) : (
+ <>
+
{note.content}
+
+
{getRelativeTime(note.createdAt)}
+
+
+
+
+
+
+ >
+ )}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/lib/api.ts b/src/lib/api.ts
index fdee3f6..dee10bd 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -1,4 +1,4 @@
-import type { Profile, Client, ClientCreate, Event, EventCreate, Email, EmailGenerate, User, Invite, ActivityItem, InsightsData, ImportPreview, ImportResult, NetworkMatch, NetworkStats } from '@/types';
+import type { Profile, Client, ClientCreate, ClientNote, 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'
@@ -404,6 +404,29 @@ class ApiClient {
}
return response.json();
}
+ // Client Notes
+ async getClientNotes(clientId: string): Promise {
+ return this.fetch(`/clients/${clientId}/notes`);
+ }
+
+ async createClientNote(clientId: string, content: string): Promise {
+ return this.fetch(`/clients/${clientId}/notes`, {
+ method: 'POST',
+ body: JSON.stringify({ content }),
+ });
+ }
+
+ async updateClientNote(clientId: string, noteId: string, data: { content?: string; pinned?: boolean }): Promise {
+ return this.fetch(`/clients/${clientId}/notes/${noteId}`, {
+ method: 'PUT',
+ body: JSON.stringify(data),
+ });
+ }
+
+ async deleteClientNote(clientId: string, noteId: string): Promise {
+ await this.fetch(`/clients/${clientId}/notes/${noteId}`, { method: 'DELETE' });
+ }
+
// Reports & Analytics
async getReportsOverview(): Promise {
return this.fetch('/reports/overview');
diff --git a/src/pages/ClientDetailPage.tsx b/src/pages/ClientDetailPage.tsx
index 43efc69..179ee31 100644
--- a/src/pages/ClientDetailPage.tsx
+++ b/src/pages/ClientDetailPage.tsx
@@ -10,11 +10,12 @@ import {
} from 'lucide-react';
import { usePinnedClients } from '@/hooks/usePinnedClients';
import { cn, formatDate, getRelativeTime, getInitials } from '@/lib/utils';
-import Badge, { EventTypeBadge, EmailStatusBadge } from '@/components/Badge';
+import Badge, { EventTypeBadge, EmailStatusBadge, StageBadge } from '@/components/Badge';
import { PageLoader } from '@/components/LoadingSpinner';
import Modal from '@/components/Modal';
import ClientForm from '@/components/ClientForm';
import EmailComposeModal from '@/components/EmailComposeModal';
+import ClientNotes from '@/components/ClientNotes';
export default function ClientDetailPage() {
const { id } = useParams<{ id: string }>();
@@ -23,7 +24,7 @@ export default function ClientDetailPage() {
const [events, setEvents] = useState([]);
const [emails, setEmails] = useState([]);
const [activities, setActivities] = useState([]);
- const [activeTab, setActiveTab] = useState<'info' | 'activity' | 'events' | 'emails'>('info');
+ const [activeTab, setActiveTab] = useState<'info' | 'notes' | 'activity' | 'events' | 'emails'>('info');
const [showEdit, setShowEdit] = useState(false);
const [showCompose, setShowCompose] = useState(false);
const [deleting, setDeleting] = useState(false);
@@ -62,8 +63,9 @@ export default function ClientDetailPage() {
setShowEdit(false);
};
- const tabs: { key: 'info' | 'activity' | 'events' | 'emails'; label: string; count?: number; icon: typeof Users }[] = [
+ const tabs: { key: typeof activeTab; label: string; count?: number; icon: typeof Users }[] = [
{ key: 'info', label: 'Info', icon: Users },
+ { key: 'notes', label: 'Notes', icon: FileText },
{ key: 'activity', label: 'Timeline', count: activities.length, icon: Activity },
{ key: 'events', label: 'Events', count: events.length, icon: Calendar },
{ key: 'emails', label: 'Emails', count: emails.length, icon: Mail },
@@ -90,11 +92,17 @@ export default function ClientDetailPage() {
{client.role ? `${client.role} at ` : ''}{client.company}
)}
- {client.tags && client.tags.length > 0 && (
-
- {client.tags.map((tag) => {tag})}
-
- )}
+
+ {
+ const stages = ['lead', 'prospect', 'onboarding', 'active', 'inactive'];
+ const currentIdx = stages.indexOf(client.stage || 'lead');
+ const nextStage = stages[(currentIdx + 1) % stages.length];
+ await updateClient(client.id, { stage: nextStage } as any);
+ }} />
+ {client.tags && client.tags.length > 0 && (
+ client.tags.map((tag) => {tag})
+ )}
+
@@ -240,6 +248,10 @@ export default function ClientDetailPage() {
)}
+ {activeTab === 'notes' && (
+
+ )}
+
{activeTab === 'activity' && (
{activities.length === 0 ? (
diff --git a/src/pages/ClientsPage.tsx b/src/pages/ClientsPage.tsx
index 9566653..a44c2ef 100644
--- a/src/pages/ClientsPage.tsx
+++ b/src/pages/ClientsPage.tsx
@@ -1,9 +1,9 @@
import { useEffect, useState, useMemo } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useClientsStore } from '@/stores/clients';
-import { Search, Plus, Users, X, Upload } from 'lucide-react';
+import { Search, Plus, Users, X, Upload, LayoutGrid, List, Kanban } from 'lucide-react';
import { cn, getRelativeTime, getInitials } from '@/lib/utils';
-import Badge from '@/components/Badge';
+import Badge, { StageBadge } from '@/components/Badge';
import EmptyState from '@/components/EmptyState';
import { PageLoader } from '@/components/LoadingSpinner';
import Modal from '@/components/Modal';
@@ -16,6 +16,9 @@ export default function ClientsPage() {
const [showCreate, setShowCreate] = useState(false);
const [showImport, setShowImport] = useState(false);
const [creating, setCreating] = useState(false);
+ const [viewMode, setViewMode] = useState<'grid' | 'pipeline'>(() =>
+ (localStorage.getItem('clients-view') as 'grid' | 'pipeline') || 'grid'
+ );
useEffect(() => {
fetchClients();
@@ -46,6 +49,27 @@ export default function ClientsPage() {
return result;
}, [clients, searchQuery, selectedTag]);
+ // Pipeline columns
+ const pipelineStages = ['lead', 'prospect', 'onboarding', 'active', 'inactive'] as const;
+ const pipelineColumns = useMemo(() => {
+ const cols: Record
= {};
+ pipelineStages.forEach(s => { cols[s] = []; });
+ filteredClients.forEach(c => {
+ const stage = c.stage || 'lead';
+ if (cols[stage]) cols[stage].push(c);
+ });
+ return cols;
+ }, [filteredClients]);
+
+ const stageCounts = useMemo(() => {
+ const counts: Record = {};
+ clients.forEach(c => {
+ const s = c.stage || 'lead';
+ counts[s] = (counts[s] || 0) + 1;
+ });
+ return counts;
+ }, [clients]);
+
// All unique tags
const allTags = useMemo(() => {
const tags = new Set();
@@ -75,6 +99,28 @@ export default function ClientsPage() {
{clients.length} contacts in your network
+
+
+
+
- {/* Client Grid */}
+ {/* Pipeline Summary Bar */}
+ {viewMode === 'grid' && clients.length > 0 && (
+
+ {pipelineStages.map((stage) => {
+ const count = stageCounts[stage] || 0;
+ const total = clients.length;
+ const pct = total > 0 ? Math.max((count / total) * 100, count > 0 ? 4 : 0) : 0;
+ const colors: Record
= {
+ lead: 'bg-slate-300 dark:bg-slate-600',
+ prospect: 'bg-blue-400 dark:bg-blue-500',
+ onboarding: 'bg-amber-400 dark:bg-amber-500',
+ active: 'bg-emerald-400 dark:bg-emerald-500',
+ inactive: 'bg-red-400 dark:bg-red-500',
+ };
+ return (
+
+
+ {stage}
+ ({count})
+
+
+
+ );
+ })}
+
+ )}
+
+ {/* Client Grid / Pipeline View */}
{filteredClients.length === 0 ? (
setShowCreate(true) } : undefined}
/>
+ ) : viewMode === 'pipeline' ? (
+ /* Pipeline / Kanban View */
+
+ {pipelineStages.map((stage) => {
+ const stageClients = pipelineColumns[stage] || [];
+ const headerColors: Record
= {
+ lead: 'border-t-slate-400',
+ prospect: 'border-t-blue-500',
+ onboarding: 'border-t-amber-500',
+ active: 'border-t-emerald-500',
+ inactive: 'border-t-red-500',
+ };
+ return (
+
+
+
{stage}
+ {stageClients.length}
+
+
+ {stageClients.map((client) => (
+
+
+
+ {getInitials(client.firstName, client.lastName)}
+
+
+
+ {client.firstName} {client.lastName}
+
+ {client.company && (
+
{client.company}
+ )}
+
+
+ {client.tags && client.tags.length > 0 && (
+
+ {client.tags.slice(0, 2).map(tag => (
+ {tag}
+ ))}
+
+ )}
+
+ {getRelativeTime(client.lastContacted)}
+
+
+ ))}
+ {stageClients.length === 0 && (
+
No clients
+ )}
+
+
+ );
+ })}
+
) : (
+ /* Grid View */
{filteredClients.map((client) => (
- {client.tags && client.tags.length > 0 && (
-
- {client.tags.slice(0, 3).map((tag) => (
-
- {tag}
-
- ))}
- {client.tags.length > 3 && (
- +{client.tags.length - 3}
- )}
-
- )}
+
+
+ {client.tags && client.tags.slice(0, 2).map((tag) => (
+
+ {tag}
+
+ ))}
+ {client.tags && client.tags.length > 2 && (
+ +{client.tags.length - 2}
+ )}
+
Last contacted: {getRelativeTime(client.lastContacted)}
diff --git a/src/types/index.ts b/src/types/index.ts
index dc41982..cf88f7d 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -40,6 +40,7 @@ export interface Client {
};
notes?: string;
tags?: string[];
+ stage?: 'lead' | 'prospect' | 'onboarding' | 'active' | 'inactive';
lastContacted?: string;
createdAt: string;
updatedAt: string;
@@ -66,6 +67,17 @@ export interface ClientCreate {
};
notes?: string;
tags?: string[];
+ stage?: 'lead' | 'prospect' | 'onboarding' | 'active' | 'inactive';
+}
+
+export interface ClientNote {
+ id: string;
+ clientId: string;
+ userId: string;
+ content: string;
+ pinned: boolean;
+ createdAt: string;
+ updatedAt: string;
}
export interface Event {