feat: real notifications, interaction logging, bulk email compose
Some checks failed
CI/CD / test (push) Failing after 20s
CI/CD / deploy (push) Has been skipped

This commit is contained in:
2026-01-30 00:48:13 +00:00
parent b43bdf3c71
commit 691e8170f3
8 changed files with 863 additions and 118 deletions

View File

@@ -2,7 +2,8 @@ import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '@/lib/api';
import type { Client, Event, Email, InsightsData } from '@/types';
import { Users, Calendar, Mail, Plus, ArrowRight, Gift, Heart, Clock, AlertTriangle, Sparkles, UserCheck, PhoneForwarded, Star } from 'lucide-react';
import type { Interaction } from '@/types';
import { Users, Calendar, Mail, Plus, ArrowRight, Gift, Heart, Clock, AlertTriangle, Sparkles, UserCheck, PhoneForwarded, Star, Phone, FileText, MoreHorizontal } from 'lucide-react';
import { formatDate, getDaysUntil, getInitials } from '@/lib/utils';
import { EventTypeBadge } from '@/components/Badge';
import { PageLoader } from '@/components/LoadingSpinner';
@@ -13,6 +14,7 @@ export default function DashboardPage() {
const [events, setEvents] = useState<Event[]>([]);
const [emails, setEmails] = useState<Email[]>([]);
const [insights, setInsights] = useState<InsightsData | null>(null);
const [recentInteractions, setRecentInteractions] = useState<Interaction[]>([]);
const [loading, setLoading] = useState(true);
const { pinnedIds, togglePin, isPinned } = usePinnedClients();
@@ -22,11 +24,13 @@ export default function DashboardPage() {
api.getEvents({ upcoming: 7 }).catch(() => []),
api.getEmails({ status: 'draft' }).catch(() => []),
api.getInsights().catch(() => null),
]).then(([c, e, em, ins]) => {
api.getRecentInteractions(5).catch(() => []),
]).then(([c, e, em, ins, ri]) => {
setClients(c);
setEvents(e);
setEmails(em);
setInsights(ins as InsightsData | null);
setRecentInteractions(ri as Interaction[]);
setLoading(false);
});
}, []);
@@ -248,6 +252,51 @@ export default function DashboardPage() {
</div>
</div>
{/* Recent Interactions */}
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl">
<div className="flex items-center gap-2 px-5 py-4 border-b border-slate-100 dark:border-slate-700">
<Phone className="w-4 h-4 text-indigo-500" />
<h2 className="font-semibold text-slate-900 dark:text-slate-100">Recent Interactions</h2>
</div>
<div className="divide-y divide-slate-100 dark:divide-slate-700">
{recentInteractions.length === 0 ? (
<p className="px-5 py-8 text-center text-sm text-slate-400 dark:text-slate-500">No interactions logged yet</p>
) : (
recentInteractions.map((interaction) => {
const typeIcons: Record<string, typeof Phone> = {
call: Phone, meeting: Users, email: Mail, note: FileText, other: MoreHorizontal,
};
const typeColors: Record<string, string> = {
call: 'text-green-600 dark:text-green-400', meeting: 'text-blue-600 dark:text-blue-400',
email: 'text-purple-600 dark:text-purple-400', note: 'text-amber-600 dark:text-amber-400', other: 'text-slate-500',
};
const Icon = typeIcons[interaction.type] || MoreHorizontal;
return (
<Link
key={interaction.id}
to={`/clients/${interaction.clientId}`}
className="flex items-center gap-3 px-5 py-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors"
>
<Icon className={`w-4 h-4 ${typeColors[interaction.type] || 'text-slate-400'}`} />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">{interaction.title}</p>
<p className="text-xs text-slate-500 dark:text-slate-400 truncate">
{interaction.client ? `${interaction.client.firstName} ${interaction.client.lastName}` : ''}
{interaction.duration ? ` · ${interaction.duration}min` : ''}
</p>
</div>
<span className="text-xs text-slate-400 dark:text-slate-500 whitespace-nowrap">
{formatDate(interaction.contactedAt)}
</span>
</Link>
);
})
)}
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-6">
{/* Recent Clients */}
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl">
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100 dark:border-slate-700">