Files
boc/landvex-admin/frontend/src/pages/Dashboard.tsx
T
Bernt aee0f09db8 landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
2026-07-05 06:41:32 +00:00

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>
);
}