wulf-pulse/app/auvik-mappings/page.tsx

480 lines
15 KiB
TypeScript
Raw Normal View History

'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { Skeleton } from '@/components/ui/skeleton';
import {
Network,
Building2,
CheckCircle,
XCircle,
AlertCircle,
Save,
Trash2,
Search,
RefreshCw
} from 'lucide-react';
import { AuvikTenantMapping } from '@/lib/types/auvik';
import { Company } from '@/lib/types/autotask';
// Simple toast implementation
const useToast = () => {
return {
toast: ({ title, description, variant }: { title: string; description: string; variant?: string }) => {
// For now, use console and alert - can be enhanced with a proper toast library later
if (variant === 'destructive') {
console.error(`${title}: ${description}`);
alert(`Error: ${description}`);
} else {
console.log(`${title}: ${description}`);
}
}
};
};
interface TenantRow extends Partial<AuvikTenantMapping> {
auvikTenantId: string;
auvikTenantName: string;
isMapped: boolean;
deviceCount?: number;
}
interface CompanyWithCounts extends Company {
nmsDeviceCount?: number;
}
export default function AuvikMappingsPage() {
const [tenants, setTenants] = useState<TenantRow[]>([]);
const [companies, setCompanies] = useState<Company[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [filterStatus, setFilterStatus] = useState<'all' | 'mapped' | 'unmapped'>('all');
const { toast } = useToast();
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
setLoading(true);
try {
// Fetch tenant mappings (including unmapped)
const mappingsRes = await fetch('/api/auvik/tenant-mappings?includeUnmapped=true');
const mappingsData = await mappingsRes.json();
// Fetch all companies
const companiesRes = await fetch('/api/companies');
const companiesData = await companiesRes.json();
// Fetch device counts for each tenant
const auvikDevicesRes = await fetch('/api/auvik/devices');
let auvikDevices: any[] = [];
if (auvikDevicesRes.ok) {
const auvikData = await auvikDevicesRes.json();
auvikDevices = auvikData.devices || [];
}
// Count devices per tenant
const deviceCountsByTenant: Record<string, number> = {};
auvikDevices.forEach((device: any) => {
const tenantId = device.tenantId;
if (tenantId) {
deviceCountsByTenant[tenantId] = (deviceCountsByTenant[tenantId] || 0) + 1;
}
});
const tenantRows: TenantRow[] = mappingsData.mappings.map((m: any) => ({
...m,
isMapped: m.autotaskCompanyId > 0,
deviceCount: deviceCountsByTenant[m.auvikTenantId] || 0,
}));
setTenants(tenantRows);
setCompanies(companiesData.companies || []);
} catch (error) {
console.error('Error fetching data:', error);
toast({
title: 'Error',
description: 'Failed to load tenant mappings',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
const handleSaveMapping = async (tenantId: string, tenantName: string, companyId: number) => {
setSaving(tenantId);
try {
const company = companies.find((c) => c.id === companyId);
if (!company) {
throw new Error('Company not found');
}
const response = await fetch('/api/auvik/tenant-mappings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
auvikTenantId: tenantId,
auvikTenantName: tenantName,
autotaskCompanyId: companyId,
autotaskCompanyName: company.companyName,
}),
});
if (!response.ok) {
throw new Error('Failed to save mapping');
}
toast({
title: 'Success',
description: `Mapped ${tenantName} to ${company.companyName}`,
});
await fetchData();
} catch (error) {
console.error('Error saving mapping:', error);
toast({
title: 'Error',
description: 'Failed to save mapping',
variant: 'destructive',
});
} finally {
setSaving(null);
}
};
const handleDeleteMapping = async (mappingId: number) => {
try {
const response = await fetch(`/api/auvik/tenant-mappings?id=${mappingId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete mapping');
}
toast({
title: 'Success',
description: 'Mapping deleted successfully',
});
await fetchData();
} catch (error) {
console.error('Error deleting mapping:', error);
toast({
title: 'Error',
description: 'Failed to delete mapping',
variant: 'destructive',
});
}
};
const filteredTenants = tenants.filter((tenant) => {
const matchesSearch =
tenant.auvikTenantName.toLowerCase().includes(searchTerm.toLowerCase()) ||
tenant.autotaskCompanyName?.toLowerCase().includes(searchTerm.toLowerCase());
const matchesFilter =
filterStatus === 'all' ||
(filterStatus === 'mapped' && tenant.isMapped) ||
(filterStatus === 'unmapped' && !tenant.isMapped);
return matchesSearch && matchesFilter;
});
const stats = {
total: tenants.length,
mapped: tenants.filter((t) => t.isMapped).length,
unmapped: tenants.filter((t) => !t.isMapped).length,
};
return (
<div className="container mx-auto py-8 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold flex items-center gap-3">
<Network className="w-8 h-8 text-blue-600" />
NMS Tenant Mappings
</h1>
<p className="text-muted-foreground mt-2">
Map NMS (Auvik) tenants to Autotask companies for device synchronization
</p>
</div>
<Button onClick={fetchData} variant="outline" size="sm">
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Tenants
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-600" />
Mapped
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{stats.mapped}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-orange-600" />
Unmapped
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">{stats.unmapped}</div>
</CardContent>
</Card>
</div>
{/* Filters */}
<Card>
<CardHeader>
<CardTitle>Tenant Mappings</CardTitle>
<CardDescription>
Select an Autotask company for each NMS tenant to enable device matching
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search tenants or companies..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<Select
value={filterStatus}
onValueChange={(value: any) => setFilterStatus(value)}
>
<SelectTrigger className="w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Tenants</SelectItem>
<SelectItem value="mapped">Mapped Only</SelectItem>
<SelectItem value="unmapped">Unmapped Only</SelectItem>
</SelectContent>
</Select>
</div>
{/* Table */}
{loading ? (
<div className="space-y-2">
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
</div>
) : (
<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[200px]">
<div className="flex items-center gap-2">
<Network className="w-4 h-4" />
NMS Tenant
</div>
</TableHead>
<TableHead>
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4" />
Autotask Company
</div>
</TableHead>
<TableHead className="w-[100px]">Status</TableHead>
<TableHead className="w-[100px] text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredTenants.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="text-center py-8 text-muted-foreground">
No tenants found
</TableCell>
</TableRow>
) : (
filteredTenants.map((tenant) => (
<TenantMappingRow
key={tenant.auvikTenantId}
tenant={tenant}
companies={companies}
saving={saving === tenant.auvikTenantId}
onSave={handleSaveMapping}
onDelete={handleDeleteMapping}
/>
))
)}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
);
}
interface TenantMappingRowProps {
tenant: TenantRow;
companies: Company[];
saving: boolean;
onSave: (tenantId: string, tenantName: string, companyId: number) => void;
onDelete: (mappingId: number) => void;
}
function TenantMappingRow({
tenant,
companies,
saving,
onSave,
onDelete,
}: TenantMappingRowProps) {
const [selectedCompanyId, setSelectedCompanyId] = useState<number>(
tenant.autotaskCompanyId || 0
);
const [hasChanges, setHasChanges] = useState(false);
const handleCompanyChange = (value: string) => {
const companyId = parseInt(value);
setSelectedCompanyId(companyId);
setHasChanges(companyId !== tenant.autotaskCompanyId);
};
const handleSave = () => {
if (selectedCompanyId > 0) {
onSave(tenant.auvikTenantId, tenant.auvikTenantName, selectedCompanyId);
setHasChanges(false);
}
};
return (
<TableRow>
<TableCell>
<div className="flex items-center justify-between">
<div>
<div className="font-medium">{tenant.auvikTenantName}</div>
<div className="text-xs text-muted-foreground font-mono">
{tenant.auvikTenantId}
</div>
</div>
{tenant.deviceCount !== undefined && tenant.deviceCount > 0 && (
<Badge variant="secondary" className="ml-2">
<Network className="w-3 h-3 mr-1" />
{tenant.deviceCount} {tenant.deviceCount === 1 ? 'device' : 'devices'}
</Badge>
)}
</div>
</TableCell>
<TableCell>
<Select
value={selectedCompanyId.toString()}
onValueChange={handleCompanyChange}
disabled={saving}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a company...">
{selectedCompanyId === 0
? "No mapping"
: companies.find(c => c.id === selectedCompanyId)?.companyName || "Select a company..."}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="0">No mapping</SelectItem>
{companies
.sort((a, b) => a.companyName.localeCompare(b.companyName))
.map((company) => (
<SelectItem key={company.id} value={company.id.toString()}>
{company.companyName}
</SelectItem>
))}
</SelectContent>
</Select>
</TableCell>
<TableCell>
{tenant.isMapped ? (
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100">
<CheckCircle className="w-3 h-3 mr-1" />
Mapped
</Badge>
) : (
<Badge variant="secondary">
<XCircle className="w-3 h-3 mr-1" />
Unmapped
</Badge>
)}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
{hasChanges && (
<Button
size="sm"
onClick={handleSave}
disabled={saving || selectedCompanyId === 0}
>
{saving ? (
<RefreshCw className="w-3 h-3 animate-spin" />
) : (
<>
<Save className="w-3 h-3 mr-1" />
Save
</>
)}
</Button>
)}
{tenant.isMapped && tenant.id && (
<Button
size="sm"
variant="ghost"
onClick={() => onDelete(tenant.id!)}
disabled={saving}
>
<Trash2 className="w-3 h-3" />
</Button>
)}
</div>
</TableCell>
</TableRow>
);
}