d8a4e43bcc
- Top header bar + narrow sidebar layout - Dashboard: calendar-style schedule with timeline cards - Clients: card-based layout with avatar initials and pet count - Services: grid cards with emoji indicators and pricing - Bookings: timeline cards with time slots, status chips, inline actions - Invoices: cards with status badges, action buttons - All pages: search bars, empty states, modal forms - Consistent color system: indigo primary, amber alerts, gray hierarchy
199 lines
9.8 KiB
TypeScript
199 lines
9.8 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import {
|
|
Users, Scissors, Calendar, FileText, DollarSign,
|
|
TrendingUp, AlertCircle,
|
|
} from 'lucide-react';
|
|
import { API_URL } from '../config';
|
|
|
|
interface Stat { label: string; value: number; sublabel: string; icon: any; color: string; path: string }
|
|
|
|
export default function Dashboard() {
|
|
const stats: Stat[] = [
|
|
{ label: 'Active Clients', value: 0, sublabel: '+12 this month', icon: Users, color: 'from-blue-500 to-blue-600', path: '/clients' },
|
|
{ label: 'Scheduled', value: 0, sublabel: '3 today', icon: Calendar, color: 'from-amber-500 to-amber-600', path: '/bookings' },
|
|
{ label: 'Unpaid', value: 0, sublabel: '4 invoices', icon: FileText, color: 'from-red-500 to-red-600', path: '/invoices' },
|
|
{ label: 'Revenue', value: 0, sublabel: 'This month', icon: DollarSign, color: 'from-emerald-500 to-emerald-600', path: '/invoices' },
|
|
];
|
|
|
|
// Fetch stats
|
|
const [statsData, setStatsData] = useState<Stat[]>([]);
|
|
const [recentBookings, setRecentBookings] = useState<any[]>([]);
|
|
|
|
useEffect(() => {
|
|
Promise.all([
|
|
fetch(`${API_URL}/clients?limit=1`).then(r => r.json().catch(() => ({ total: 0 }))),
|
|
fetch(`${API_URL}/bookings?status=scheduled`).then(r => r.json().catch(() => ({ items: [] }))),
|
|
fetch(`${API_URL}/invoices`).then(r => r.json().catch(() => ({ items: [] }))),
|
|
fetch(`${API_URL}/services`).then(r => r.json().catch(() => ({ items: [] }))),
|
|
fetch(`${API_URL}/bookings?limit=5`).then(r => r.json().catch(() => ({ items: [] }))),
|
|
]).then(([clients, bookings, invoices, services, recent]) => {
|
|
const paidCount = (invoices.items || []).filter((x: any) => x.status !== 'paid').length;
|
|
const unpaidTotal = (invoices.items || []).reduce((sum: number, x: any) => sum + (x.total || 0), 0);
|
|
setStatsData([
|
|
{ ...stats[0], value: clients.total || 0 },
|
|
{ ...stats[1], value: bookings.items?.filter((x: any) => x.status === 'scheduled').length || 0 },
|
|
{ ...stats[2], value: paidCount, sublabel: `${paidCount} invoices` },
|
|
{ ...stats[3], value: unpaidTotal / 100 || 0, sublabel: 'This month' },
|
|
]);
|
|
setRecentBookings(recent.items || []);
|
|
}).catch(() => {});
|
|
}, []);
|
|
|
|
const statusColors: Record<string, { bg: string; text: string }> = {
|
|
scheduled: { bg: 'bg-amber-100', text: 'text-amber-700' },
|
|
confirmed: { bg: 'bg-blue-100', text: 'text-blue-700' },
|
|
in_progress: { bg: 'bg-purple-100', text: 'text-purple-700' },
|
|
completed: { bg: 'bg-green-100', text: 'text-green-700' },
|
|
cancelled: { bg: 'bg-red-100', text: 'text-red-700' },
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
{/* Top Bar */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-xl font-bold tracking-tight text-slate-800">Dashboard</h1>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
{new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}
|
|
</p>
|
|
</div>
|
|
<Link to="/bookings" className="bg-indigo-600 text-white text-sm font-semibold px-4 py-2 rounded-lg hover:bg-indigo-700 transition-colors flex items-center gap-2">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
New Booking
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Stats Row */}
|
|
<div className="grid grid-cols-4 gap-4 mb-6">
|
|
{statsData.map((s, i) => (
|
|
<Link key={i} to={s.path} className="block">
|
|
<div className="bg-white rounded-xl p-5 border border-gray-200 hover:shadow-md transition-shadow">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className={`w-10 h-10 rounded-xl bg-gradient-to-br ${s.color} flex items-center justify-center`}>
|
|
<s.icon className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div className="text-[10px] uppercase tracking-wider text-gray-500 font-medium">{s.label}</div>
|
|
</div>
|
|
<div className="text-2xl font-bold text-slate-800">
|
|
{s.label === 'Revenue' ? `$${s.value}` : s.value}
|
|
</div>
|
|
<div className="text-xs text-gray-400 mt-1">{s.sublabel}</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
{/* Two Column: Schedule + Alerts */}
|
|
<div className="grid grid-cols-3 gap-4">
|
|
{/* Left: Today's Schedule (2 cols) */}
|
|
<div className="col-span-2 bg-white rounded-xl border border-gray-200">
|
|
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
<h2 className="text-sm font-bold text-slate-800 flex items-center gap-2">
|
|
<Calendar className="w-4 h-4 text-indigo-600" />
|
|
Today's Schedule
|
|
</h2>
|
|
<Link to="/bookings" className="text-xs text-indigo-600 hover:text-indigo-700 font-medium">
|
|
View Full Calendar →
|
|
</Link>
|
|
</div>
|
|
{recentBookings.length === 0 ? (
|
|
<div className="p-8 text-center">
|
|
<div className="text-4xl mb-3">📅</div>
|
|
<p className="text-sm text-gray-400 font-medium">No bookings today</p>
|
|
<Link to="/bookings" className="text-xs text-indigo-600 hover:text-indigo-700 font-medium mt-2 inline-block">
|
|
Create your first booking →
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div className="divide-y divide-gray-100">
|
|
{recentBookings.map((booking) => {
|
|
const sc = statusColors[booking.status] || statusColors.scheduled;
|
|
return (
|
|
<Link
|
|
key={booking.id}
|
|
to="/bookings"
|
|
className="p-4 flex items-center gap-4 hover:bg-gray-50 transition-colors"
|
|
>
|
|
<div className="text-xs font-mono text-gray-400 w-20">
|
|
{booking.startAt ? new Date(booking.startAt).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }) : '—'}
|
|
</div>
|
|
<div className="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-sm">
|
|
🐾
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-semibold text-slate-800 truncate">
|
|
{booking.client?.firstName} {booking.client?.lastName}
|
|
</div>
|
|
<div className="text-xs text-gray-500">
|
|
{booking.service?.name} · {booking.assignedStaff || 'Unassigned'}
|
|
</div>
|
|
</div>
|
|
<span className={`px-2.5 py-1 rounded-full text-[11px] font-semibold ${sc.bg} ${sc.text}`}>
|
|
{booking.status}
|
|
</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right: Quick Actions + Alerts */}
|
|
<div className="space-y-4">
|
|
<div className="bg-white rounded-xl border border-gray-200">
|
|
<div className="p-4 border-b border-gray-200">
|
|
<h2 className="text-sm font-bold text-slate-800">Quick Actions</h2>
|
|
</div>
|
|
<div className="p-3 space-y-1.5">
|
|
<Link to="/clients" className="flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg hover:bg-gray-50 text-gray-600 transition-colors">
|
|
<div className="w-8 h-8 rounded-lg bg-blue-100 flex items-center justify-center">
|
|
<Users className="w-4 h-4 text-blue-600" />
|
|
</div>
|
|
Add Client
|
|
</Link>
|
|
<Link to="/bookings" className="flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg hover:bg-gray-50 text-gray-600 transition-colors">
|
|
<div className="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center">
|
|
<Calendar className="w-4 h-4 text-amber-600" />
|
|
</div>
|
|
Create Booking
|
|
</Link>
|
|
<Link to="/invoices" className="flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg hover:bg-gray-50 text-gray-600 transition-colors">
|
|
<div className="w-8 h-8 rounded-lg bg-green-100 flex items-center justify-center">
|
|
<FileText className="w-4 h-4 text-green-600" />
|
|
</div>
|
|
Generate Invoice
|
|
</Link>
|
|
<Link to="/messages" className="flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg hover:bg-gray-50 text-gray-600 transition-colors">
|
|
<div className="w-8 h-8 rounded-lg bg-purple-100 flex items-center justify-center">
|
|
<MessageCircle className="w-4 h-4 text-purple-600" />
|
|
</div>
|
|
Send Message
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-amber-50 rounded-xl border border-amber-200 p-4">
|
|
<h2 className="text-sm font-bold text-amber-800 flex items-center gap-2">
|
|
<AlertCircle className="w-4 h-4" />
|
|
Alerts
|
|
</h2>
|
|
<div className="mt-3 space-y-2">
|
|
<p className="text-xs text-amber-600/80 flex items-start gap-2">
|
|
<span className="mt-1 w-1.5 h-1.5 rounded-full bg-amber-400 flex-shrink-0" />
|
|
2 invoices overdue
|
|
</p>
|
|
<p className="text-xs text-amber-600/80 flex items-start gap-2">
|
|
<span className="mt-1 w-1.5 h-1.5 rounded-full bg-amber-400 flex-shrink-0" />
|
|
1 pet vaccination expiring
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|