aee0f09db8
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
184 lines
5.7 KiB
TypeScript
184 lines
5.7 KiB
TypeScript
import React from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import {
|
|
Building2,
|
|
Users,
|
|
Activity,
|
|
CreditCard,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
} from 'lucide-react';
|
|
import { api } from '@/lib/api';
|
|
import { usePermissions } from '@/hooks/usePermissions';
|
|
|
|
interface DashboardStats {
|
|
tenants: { total: number; active: number; trial: number };
|
|
users: { total: number; active: number };
|
|
api_usage: { today: number; this_month: number; limit: number };
|
|
revenue: { mrr: number; this_month: number; last_month: number };
|
|
}
|
|
|
|
function StatCard({
|
|
title,
|
|
value,
|
|
subtitle,
|
|
icon: Icon,
|
|
trend,
|
|
trendUp,
|
|
}: {
|
|
title: string;
|
|
value: string | number;
|
|
subtitle: string;
|
|
icon: React.ElementType;
|
|
trend?: string;
|
|
trendUp?: boolean;
|
|
}) {
|
|
return (
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-600">{title}</p>
|
|
<p className="mt-2 text-3xl font-bold text-gray-900">{value}</p>
|
|
{trend && (
|
|
<div className={`mt-1 flex items-center text-sm ${trendUp ? 'text-green-600' : 'text-red-600'}`}>
|
|
{trendUp ? <TrendingUp className="w-4 h-4 mr-1" /> : <TrendingDown className="w-4 h-4 mr-1" />}
|
|
{trend}
|
|
</div>
|
|
)}
|
|
<p className="mt-1 text-sm text-gray-500">{subtitle}</p>
|
|
</div>
|
|
<div className="p-3 bg-indigo-50 rounded-lg">
|
|
<Icon className="w-6 h-6 text-indigo-600" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Dashboard() {
|
|
const { hasPermission } = usePermissions();
|
|
|
|
const { data: stats } = useQuery<DashboardStats>({
|
|
queryKey: ['dashboard', 'summary'],
|
|
queryFn: () => api.get('/dashboard/summary'),
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
|
<button className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{hasPermission('tenants.read') && (
|
|
<StatCard
|
|
title="Active Tenants"
|
|
value={stats?.tenants.active ?? 0}
|
|
subtitle={`${stats?.tenants.total ?? 0} total`}
|
|
icon={Building2}
|
|
trend="↑ 12%"
|
|
trendUp
|
|
/>
|
|
)}
|
|
<StatCard
|
|
title="Total Users"
|
|
value={stats?.users.total ?? 0}
|
|
subtitle={`${stats?.users.active ?? 0} active`}
|
|
icon={Users}
|
|
trend="↑ 8%"
|
|
trendUp
|
|
/>
|
|
<StatCard
|
|
title="API Calls Today"
|
|
value={stats?.api_usage.today?.toLocaleString() ?? 0}
|
|
subtitle={`${stats?.api_usage.this_month?.toLocaleString() ?? 0} this month`}
|
|
icon={Activity}
|
|
trend="↑ 23%"
|
|
trendUp
|
|
/>
|
|
{hasPermission('billing.read') && (
|
|
<StatCard
|
|
title="Revenue (MTD)"
|
|
value={`${stats?.revenue.this_month?.toLocaleString() ?? 0} SEK`}
|
|
subtitle={`MRR: ${stats?.revenue.mrr?.toLocaleString() ?? 0} SEK`}
|
|
icon={CreditCard}
|
|
trend="↑ 15%"
|
|
trendUp
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Recent Activity */}
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h2 className="text-lg font-medium text-gray-900">Recent Activity</h2>
|
|
</div>
|
|
<div className="p-6">
|
|
<ActivityFeed />
|
|
</div>
|
|
</div>
|
|
|
|
{/* API Usage Chart */}
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h2 className="text-lg font-medium text-gray-900">API Usage (7 days)</h2>
|
|
</div>
|
|
<div className="p-6">
|
|
<ApiUsageChart />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ActivityFeed() {
|
|
const activities = [
|
|
{ action: 'New tenant', detail: 'Stockholm Kommun registered', time: '2 min ago' },
|
|
{ action: 'User locked', detail: 'admin@example.com after 5 failed attempts', time: '15 min ago' },
|
|
{ action: 'Invoice paid', detail: '#INV-2024-001 - 12,000 SEK', time: '1 hour ago' },
|
|
{ action: 'API key revoked', detail: 'Production key by admin@x.com', time: '2 hours ago' },
|
|
];
|
|
|
|
return (
|
|
<ul className="space-y-4">
|
|
{activities.map((activity, i) => (
|
|
<li key={i} className="flex items-start space-x-3">
|
|
<div className="w-2 h-2 mt-2 bg-indigo-500 rounded-full" />
|
|
<div>
|
|
<p className="text-sm font-medium text-gray-900">{activity.action}</p>
|
|
<p className="text-sm text-gray-500">{activity.detail}</p>
|
|
<p className="text-xs text-gray-400 mt-1">{activity.time}</p>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
function ApiUsageChart() {
|
|
// Placeholder for chart - would use recharts or similar
|
|
const data = [65, 78, 90, 81, 56, 95, 120];
|
|
const max = Math.max(...data);
|
|
|
|
return (
|
|
<div className="h-64 flex items-end space-x-2">
|
|
{data.map((value, i) => (
|
|
<div key={i} className="flex-1 flex flex-col items-center">
|
|
<div
|
|
className="w-full bg-indigo-500 rounded-t transition-all hover:bg-indigo-600"
|
|
style={{ height: `${(value / max) * 100}%` }}
|
|
/>
|
|
<span className="text-xs text-gray-500 mt-2">
|
|
{['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][i]}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|