Added comprehensive authentication and authorization system: Authentication System: - Better Auth integration with session management - Login/logout pages and API routes - Middleware for route protection - Auth utilities and client libraries User Management: - User list, detail, and invite pages - User API endpoints (CRUD operations) - Session management for users - Profile settings page Role-Based Access Control: - Role management pages (list, create, edit) - Permission system with granular controls - Role assignment to users - Role API endpoints Admin Features: - Audit log page for tracking system events - Admin settings page - Audit service for logging user actions Additional Features: - Quotes management pages and components - SalesBldr API integration - Email service for notifications Configuration & Documentation: - Updated docker-compose.yml - MCP server configuration (mcp.json) - CVE-2025-55182 security review documentation - Standards guide and PRD documents - Re-enabling authentication documentation Database Migrations: - 012: Auth tables (users, sessions, accounts, verifications) - 013: Role tables (roles, permissions, role_permissions, user_roles) - 014: Admin settings table UI Updates: - Updated dashboard layout - Enhanced app layout with auth integration
410 lines
14 KiB
TypeScript
410 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Progress } from '@/components/ui/progress';
|
|
import {
|
|
Server,
|
|
Building2,
|
|
Network,
|
|
Globe,
|
|
Smartphone,
|
|
Database,
|
|
RefreshCw,
|
|
ArrowRight,
|
|
Activity,
|
|
TrendingUp,
|
|
AlertCircle,
|
|
CheckCircle,
|
|
XCircle,
|
|
Users,
|
|
HardDrive,
|
|
Wifi,
|
|
FileText
|
|
} from 'lucide-react';
|
|
|
|
interface DashboardStats {
|
|
companies: {
|
|
total: number;
|
|
active: number;
|
|
};
|
|
configurationItems: {
|
|
total: number;
|
|
active: number;
|
|
};
|
|
mappings: {
|
|
auvik: {
|
|
mapped: number;
|
|
unmapped: number;
|
|
};
|
|
rmm: {
|
|
mapped: number;
|
|
unmapped: number;
|
|
};
|
|
};
|
|
quotes: {
|
|
open: number;
|
|
total: number;
|
|
};
|
|
}
|
|
|
|
export default function DashboardPage() {
|
|
const [stats, setStats] = useState<DashboardStats>({
|
|
companies: { total: 0, active: 0 },
|
|
configurationItems: { total: 0, active: 0 },
|
|
mappings: {
|
|
auvik: { mapped: 0, unmapped: 0 },
|
|
rmm: { mapped: 0, unmapped: 0 }
|
|
},
|
|
quotes: { open: 0, total: 0 }
|
|
});
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetchStats();
|
|
}, []);
|
|
|
|
const fetchStats = async () => {
|
|
try {
|
|
// Fetch companies
|
|
const companiesRes = await fetch('/api/companies');
|
|
const companiesData = await companiesRes.json();
|
|
|
|
// Fetch Auvik mappings
|
|
const auvikRes = await fetch('/api/auvik/tenant-mappings?includeUnmapped=true');
|
|
const auvikData = await auvikRes.json();
|
|
|
|
// Fetch RMM mappings
|
|
const rmmRes = await fetch('/api/rmm/site-mappings?includeUnmapped=true');
|
|
const rmmData = await rmmRes.json();
|
|
|
|
// Fetch SalesBldr quotes
|
|
let quotesData = { results: [], total: 0 };
|
|
try {
|
|
const quotesRes = await fetch('/api/salesbldr/quotes?status=open&size=100');
|
|
if (quotesRes.ok) {
|
|
quotesData = await quotesRes.json();
|
|
}
|
|
} catch (quotesError) {
|
|
console.error('Error fetching quotes:', quotesError);
|
|
}
|
|
|
|
setStats({
|
|
companies: {
|
|
total: companiesData.companies?.length || 0,
|
|
active: companiesData.companies?.filter((c: any) => c.isActive).length || 0
|
|
},
|
|
configurationItems: {
|
|
total: 0, // Would need to fetch this
|
|
active: 0
|
|
},
|
|
mappings: {
|
|
auvik: {
|
|
mapped: auvikData.stats?.mapped || 0,
|
|
unmapped: auvikData.stats?.unmapped || 0
|
|
},
|
|
rmm: {
|
|
mapped: rmmData.stats?.mapped || 0,
|
|
unmapped: rmmData.stats?.unmapped || 0
|
|
}
|
|
},
|
|
quotes: {
|
|
open: quotesData.results?.length || 0,
|
|
total: quotesData.total || 0
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error fetching dashboard stats:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const quickLinks = [
|
|
{
|
|
title: 'Configuration Items',
|
|
description: 'View and manage IT assets and devices',
|
|
href: '/configuration-items',
|
|
icon: Server,
|
|
color: 'blue',
|
|
stats: `${stats.configurationItems.active} active items`
|
|
},
|
|
{
|
|
title: 'Sync Management',
|
|
description: 'Synchronize data from external systems',
|
|
href: '/admin/sync',
|
|
icon: RefreshCw,
|
|
color: 'green',
|
|
stats: 'Run data synchronization'
|
|
},
|
|
{
|
|
title: 'Data Browser',
|
|
description: 'Browse and query system data',
|
|
href: '/admin/data-browser',
|
|
icon: Database,
|
|
color: 'purple',
|
|
stats: 'Explore database tables'
|
|
}
|
|
];
|
|
|
|
const mappingCards = [
|
|
{
|
|
title: 'NMS Mapping (Auvik)',
|
|
description: 'Network Management System integration',
|
|
href: '/auvik-mappings',
|
|
icon: Network,
|
|
color: 'blue',
|
|
mapped: stats.mappings.auvik.mapped,
|
|
unmapped: stats.mappings.auvik.unmapped,
|
|
total: stats.mappings.auvik.mapped + stats.mappings.auvik.unmapped
|
|
},
|
|
{
|
|
title: 'RMM Mapping (Datto)',
|
|
description: 'Remote Monitoring & Management',
|
|
href: '/rmm-mappings',
|
|
icon: Globe,
|
|
color: 'purple',
|
|
mapped: stats.mappings.rmm.mapped,
|
|
unmapped: stats.mappings.rmm.unmapped,
|
|
total: stats.mappings.rmm.mapped + stats.mappings.rmm.unmapped
|
|
},
|
|
{
|
|
title: 'Apple RMM (Addigy)',
|
|
description: 'Apple device management',
|
|
href: '/addigy-mappings',
|
|
icon: Smartphone,
|
|
color: 'orange',
|
|
mapped: 0,
|
|
unmapped: 0,
|
|
total: 0,
|
|
comingSoon: true
|
|
}
|
|
];
|
|
|
|
const getMappingProgress = (mapped: number, total: number) => {
|
|
if (total === 0) return 0;
|
|
return (mapped / total) * 100;
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto py-8 space-y-8">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-4xl font-bold">Dashboard</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
Welcome to Pulse - Your PSA Management System
|
|
</p>
|
|
</div>
|
|
<Button onClick={fetchStats} variant="outline" size="sm" disabled={loading}>
|
|
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Stats Overview */}
|
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-6">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Total Companies</CardTitle>
|
|
<Building2 className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.companies.total}</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{stats.companies.active} active
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">NMS Coverage</CardTitle>
|
|
<Wifi className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{stats.mappings.auvik.mapped + stats.mappings.auvik.unmapped > 0
|
|
? Math.round(getMappingProgress(stats.mappings.auvik.mapped, stats.mappings.auvik.mapped + stats.mappings.auvik.unmapped))
|
|
: 0}%
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{stats.mappings.auvik.mapped} of {stats.mappings.auvik.mapped + stats.mappings.auvik.unmapped} tenants
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">RMM Coverage</CardTitle>
|
|
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{stats.mappings.rmm.mapped + stats.mappings.rmm.unmapped > 0
|
|
? Math.round(getMappingProgress(stats.mappings.rmm.mapped, stats.mappings.rmm.mapped + stats.mappings.rmm.unmapped))
|
|
: 0}%
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{stats.mappings.rmm.mapped} of {stats.mappings.rmm.mapped + stats.mappings.rmm.unmapped} sites
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Link href="/quotes">
|
|
<Card className="hover:shadow-lg transition-shadow cursor-pointer">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Open Quotes</CardTitle>
|
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.quotes.open}</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Pending approval
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">System Status</CardTitle>
|
|
<Activity className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold flex items-center gap-2">
|
|
<CheckCircle className="h-5 w-5 text-green-600" />
|
|
Online
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
All systems operational
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Quick Links */}
|
|
<div>
|
|
<h2 className="text-2xl font-bold mb-4">Quick Access</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{quickLinks.map((link) => (
|
|
<Link key={link.href} href={link.href}>
|
|
<Card className="hover:shadow-lg transition-shadow cursor-pointer h-full">
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<link.icon className={`h-8 w-8 text-${link.color}-600`} />
|
|
<ArrowRight className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
<CardTitle className="mt-4">{link.title}</CardTitle>
|
|
<CardDescription>{link.description}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-muted-foreground">{link.stats}</p>
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mapping Status */}
|
|
<div>
|
|
<h2 className="text-2xl font-bold mb-4">Integration Mappings</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{mappingCards.map((mapping) => (
|
|
<Card key={mapping.href} className="relative">
|
|
{mapping.comingSoon && (
|
|
<Badge className="absolute top-4 right-4" variant="secondary">
|
|
Coming Soon
|
|
</Badge>
|
|
)}
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<mapping.icon className={`h-8 w-8 text-${mapping.color}-600`} />
|
|
{!mapping.comingSoon && mapping.unmapped > 0 && (
|
|
<Badge variant="outline" className="bg-orange-50 border-orange-200 text-orange-700">
|
|
<AlertCircle className="h-3 w-3 mr-1" />
|
|
{mapping.unmapped} unmapped
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<CardTitle className="mt-4">{mapping.title}</CardTitle>
|
|
<CardDescription>{mapping.description}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{!mapping.comingSoon ? (
|
|
<>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-muted-foreground">Coverage</span>
|
|
<span className="font-medium">
|
|
{Math.round(getMappingProgress(mapping.mapped, mapping.total))}%
|
|
</span>
|
|
</div>
|
|
<Progress value={getMappingProgress(mapping.mapped, mapping.total)} />
|
|
</div>
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="flex items-center gap-1">
|
|
<CheckCircle className="h-3 w-3 text-green-600" />
|
|
{mapping.mapped} mapped
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<XCircle className="h-3 w-3 text-gray-400" />
|
|
{mapping.unmapped} unmapped
|
|
</span>
|
|
</div>
|
|
<Link href={mapping.href}>
|
|
<Button className="w-full" variant="outline" size="sm">
|
|
Manage Mappings
|
|
<ArrowRight className="h-4 w-4 ml-2" />
|
|
</Button>
|
|
</Link>
|
|
</>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">
|
|
Integration under development
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Activity - Placeholder */}
|
|
<div>
|
|
<h2 className="text-2xl font-bold mb-4">Recent Activity</h2>
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className="h-2 w-2 bg-green-600 rounded-full" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">Data sync completed</p>
|
|
<p className="text-xs text-muted-foreground">Companies synchronized successfully - 5 minutes ago</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<div className="h-2 w-2 bg-blue-600 rounded-full" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">New RMM site mapped</p>
|
|
<p className="text-xs text-muted-foreground">Site "Acme Corp - Dallas" mapped to Acme Corp - 2 hours ago</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<div className="h-2 w-2 bg-purple-600 rounded-full" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">Configuration items updated</p>
|
|
<p className="text-xs text-muted-foreground">247 devices synchronized from RMM - 1 day ago</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|