feat: client pipeline view + notes tab + stage badges

- Pipeline/kanban view on Clients page (toggle grid/pipeline)
- Pipeline summary bar showing client distribution across stages
- Stage badge on client cards and detail page (click to cycle)
- Notes tab on ClientDetailPage with add/edit/pin/delete
- StageBadge component with color-coded labels
- Stage selector in ClientForm
- API client methods for notes CRUD
This commit is contained in:
2026-01-30 00:35:50 +00:00
parent 38761586e7
commit b43bdf3c71
7 changed files with 455 additions and 24 deletions

View File

@@ -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<Event[]>([]);
const [emails, setEmails] = useState<Email[]>([]);
const [activities, setActivities] = useState<ActivityItem[]>([]);
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}
</p>
)}
{client.tags && client.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{client.tags.map((tag) => <Badge key={tag} color="blue">{tag}</Badge>)}
</div>
)}
<div className="flex flex-wrap gap-1.5 mt-2">
<StageBadge stage={client.stage} onClick={async () => {
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) => <Badge key={tag} color="blue">{tag}</Badge>)
)}
</div>
</div>
</div>
</div>
@@ -240,6 +248,10 @@ export default function ClientDetailPage() {
</div>
)}
{activeTab === 'notes' && (
<ClientNotes clientId={client.id} />
)}
{activeTab === 'activity' && (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl">
{activities.length === 0 ? (