wulf-pulse/autotask-app/app/config-enrichment-test/page.tsx
2025-10-28 11:21:04 -04:00

509 lines
20 KiB
TypeScript

'use client';
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { ThemeToggle } from '@/components/theme-toggle';
import {
Search,
Server,
FileText,
DollarSign,
Calendar,
AlertCircle,
CheckCircle,
Ticket,
Info
} from 'lucide-react';
import { format } from 'date-fns';
interface EnrichmentData {
configItem: any;
billingItems: any[];
invoices: any[];
tickets: any[];
summary: {
totalBilled: number;
purchaseDate?: string;
lastInvoiceDate?: string;
ticketCount: number;
};
}
export default function ConfigEnrichmentTestPage() {
const [configItemId, setConfigItemId] = useState('');
const [invoiceId, setInvoiceId] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<EnrichmentData | null>(null);
const handleSearch = async () => {
if (!configItemId && !invoiceId) return;
setLoading(true);
setError(null);
try {
const params = new URLSearchParams();
if (invoiceId) {
params.append('invoiceId', invoiceId);
} else if (configItemId) {
params.append('configItemId', configItemId);
}
const response = await fetch(`/api/config-enrichment?${params.toString()}`);
if (!response.ok) {
throw new Error('Failed to fetch enrichment data');
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 items-center">
<div className="flex flex-1 items-center justify-between">
<div className="flex items-center space-x-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-blue-600 to-blue-700 text-white shadow-lg">
<Search className="h-5 w-5" />
</div>
<div>
<h1 className="text-xl font-semibold tracking-tight">
Config Item Enrichment Test
</h1>
<p className="text-xs text-muted-foreground">
Find invoices and tickets by serial number
</p>
</div>
</div>
<ThemeToggle />
</div>
</div>
</header>
{/* Main Content */}
<main className="container mx-auto px-4 py-8 space-y-6">
{/* Search Card */}
<Card className="border-0 shadow-lg">
<CardHeader>
<CardTitle>Search Configuration Item</CardTitle>
<CardDescription>
Enter a Configuration Item ID to find related invoices and tickets (last 90 days)
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="configItemId">Configuration Item ID</Label>
<Input
id="configItemId"
type="number"
placeholder="e.g., 29872931"
value={configItemId}
onChange={(e) => {
setConfigItemId(e.target.value);
if (e.target.value) setInvoiceId('');
}}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<div>
<Label htmlFor="invoiceId">OR Invoice ID</Label>
<Input
id="invoiceId"
type="number"
placeholder="e.g., 29884310"
value={invoiceId}
onChange={(e) => {
setInvoiceId(e.target.value);
if (e.target.value) setConfigItemId('');
}}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
</div>
<Button onClick={handleSearch} disabled={loading || (!configItemId && !invoiceId)} className="w-full">
{loading ? (
<>
<Search className="w-4 h-4 mr-2 animate-spin" />
Searching...
</>
) : (
<>
<Search className="w-4 h-4 mr-2" />
Search
</>
)}
</Button>
</div>
</CardContent>
</Card>
{/* Error Display */}
{error && (
<Card className="border-red-500">
<CardContent className="pt-6">
<div className="flex items-center gap-2 text-red-500">
<AlertCircle className="w-5 h-5" />
<p>Error: {error}</p>
</div>
</CardContent>
</Card>
)}
{/* Loading State */}
{loading && (
<div className="space-y-4">
<Skeleton className="h-32 w-full" />
<Skeleton className="h-64 w-full" />
</div>
)}
{/* Results */}
{data && !loading && (
<>
{/* Config Item Info */}
<Card className="border-0 shadow-lg">
<CardHeader className="bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
<CardTitle className="flex items-center gap-2">
<Server className="w-5 h-5" />
Configuration Item Details
</CardTitle>
</CardHeader>
<CardContent className="pt-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<Label>Device Name</Label>
<p className="text-sm font-medium">{data.configItem?.referenceTitle || '-'}</p>
</div>
<div>
<Label>Serial Number</Label>
<p className="text-sm font-mono">{data.configItem?.serialNumber || '-'}</p>
</div>
<div>
<Label>Install Date</Label>
<p className="text-sm">
{data.configItem?.installDate ?
format(new Date(data.configItem.installDate), 'MMM d, yyyy') :
'-'}
</p>
</div>
<div>
<Label>Status</Label>
<Badge variant={data.configItem?.isActive ? 'default' : 'secondary'}>
{data.configItem?.isActive ? 'Active' : 'Inactive'}
</Badge>
</div>
</div>
</CardContent>
</Card>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="border-0 shadow-lg">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Total Billed</p>
<p className="text-2xl font-bold">
${data.summary.totalBilled.toFixed(2)}
</p>
</div>
<DollarSign className="h-8 w-8 text-green-600" />
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-lg">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Invoices Found</p>
<p className="text-2xl font-bold">{data.invoices.length}</p>
</div>
<FileText className="h-8 w-8 text-blue-600" />
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-lg">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Tickets Found</p>
<p className="text-2xl font-bold">{data.summary.ticketCount}</p>
</div>
<Ticket className="h-8 w-8 text-orange-600" />
</div>
</CardContent>
</Card>
<Card className="border-0 shadow-lg">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Purchase Date</p>
<p className="text-sm font-medium">
{data.summary.purchaseDate ?
format(new Date(data.summary.purchaseDate), 'MMM d, yyyy') :
'-'}
</p>
</div>
<Calendar className="h-8 w-8 text-purple-600" />
</div>
</CardContent>
</Card>
</div>
{/* Invoices */}
{data.invoices.length > 0 && (
<Card className="border-0 shadow-lg">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="w-5 h-5" />
Company Invoices (Last 90 Days)
</CardTitle>
<CardDescription>
All invoices for {data.configItem?.companyName || 'this company'}
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Invoice #</TableHead>
<TableHead>Date</TableHead>
<TableHead>Total</TableHead>
<TableHead>Status</TableHead>
<TableHead>Due Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.invoices.map((invoice) => (
<TableRow key={invoice.id}>
<TableCell>
<Badge variant="outline">{invoice.invoiceNumber || invoice.id}</Badge>
</TableCell>
<TableCell>
{invoice.invoiceDateTime ?
format(new Date(invoice.invoiceDateTime), 'MMM d, yyyy') :
'-'}
</TableCell>
<TableCell className="font-medium">
${(invoice.invoiceTotal || 0).toFixed(2)}
</TableCell>
<TableCell>
<Badge variant={invoice.paidDate ? 'default' : 'secondary'}>
{invoice.paidDate ? 'Paid' : 'Unpaid'}
</Badge>
</TableCell>
<TableCell>
{invoice.dueDate ?
format(new Date(invoice.dueDate), 'MMM d, yyyy') :
'-'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
{/* Billing Items */}
{data.billingItems.length > 0 && (
<Card className="border-0 shadow-lg">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="w-5 h-5" />
{data.configItem ? 'Purchase History for This Device' : 'Hardware Purchases'}
</CardTitle>
<CardDescription>
{data.configItem
? `Billing items matching serial number ${data.configItem.serialNumber} (last 90 days)`
: 'Hardware line items from invoice'}
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead>Description</TableHead>
<TableHead>Serial/Notes</TableHead>
<TableHead>Qty</TableHead>
<TableHead>Unit Price</TableHead>
<TableHead>Total</TableHead>
<TableHead>Invoice</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.billingItems.map((item, index) => (
<TableRow key={index}>
<TableCell className="whitespace-nowrap">
{item.itemDate ? format(new Date(item.itemDate), 'MMM d, yyyy') : '-'}
</TableCell>
<TableCell className="max-w-md">
<div className="font-medium">{item.description || '-'}</div>
{item.itemName && item.itemName !== item.description && (
<div className="text-xs text-muted-foreground mt-1">{item.itemName}</div>
)}
</TableCell>
<TableCell className="max-w-xs">
<div className="text-xs space-y-1">
{item.serialNumber && (
<div className="font-mono bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
SN: {item.serialNumber}
</div>
)}
{item.internalNotes && (
<div className="text-muted-foreground">{item.internalNotes}</div>
)}
{item.vendorInvoiceNumber && (
<div className="text-muted-foreground">Vendor: {item.vendorInvoiceNumber}</div>
)}
</div>
</TableCell>
<TableCell>{item.quantity || 1}</TableCell>
<TableCell className="whitespace-nowrap">${(item.unitPrice || 0).toFixed(2)}</TableCell>
<TableCell className="font-medium whitespace-nowrap">
${(item.totalAmount || 0).toFixed(2)}
</TableCell>
<TableCell>
<Badge variant="outline">{item.invoiceID}</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
{/* Tickets */}
{data.tickets.length > 0 && (
<Card className="border-0 shadow-lg">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Ticket className="w-5 h-5" />
Related Tickets
</CardTitle>
<CardDescription>
Tickets mentioning this serial number (last 90 days)
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Ticket #</TableHead>
<TableHead>Title</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead>Priority</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.tickets.map((ticket) => (
<TableRow key={ticket.id}>
<TableCell>
<Badge variant="outline">{ticket.ticketNumber}</Badge>
</TableCell>
<TableCell>{ticket.title}</TableCell>
<TableCell>
<Badge>{ticket.status}</Badge>
</TableCell>
<TableCell>
{format(new Date(ticket.createDate), 'MMM d, yyyy')}
</TableCell>
<TableCell>
<Badge variant={ticket.priority === 'Critical' ? 'destructive' : 'secondary'}>
{ticket.priority}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)}
{/* Debug: Show all fields from first billing item - only when searching by invoice */}
{data.billingItems.length > 0 && !data.configItem && invoiceId && (
<Card className="border-0 shadow-lg border-l-4 border-l-blue-500">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Info className="w-5 h-5" />
Debug: Available Fields (First Item)
</CardTitle>
<CardDescription>
All fields available in BillingItems entity
</CardDescription>
</CardHeader>
<CardContent>
<pre className="text-xs bg-gray-100 dark:bg-gray-900 p-4 rounded overflow-auto max-h-96">
{JSON.stringify(data.billingItems[0], null, 2)}
</pre>
</CardContent>
</Card>
)}
{/* No Results */}
{data.billingItems.length === 0 && data.tickets.length === 0 && (
<Card className="border-0 shadow-lg">
<CardContent className="pt-6">
<div className="text-center py-12 text-muted-foreground">
<AlertCircle className="w-12 h-12 mx-auto mb-4 opacity-50" />
{data.configItem ? (
<>
<p className="font-medium">No purchase records found for this device</p>
<p className="text-sm mt-2">
Serial number: {data.configItem.serialNumber || 'Not set'}
</p>
<p className="text-sm mt-2">
This device may have been:
</p>
<ul className="text-sm mt-2 space-y-1">
<li> Purchased more than 1 year ago</li>
<li> Added manually without an invoice</li>
<li> Invoiced without serial number in description</li>
</ul>
</>
) : (
<>
<p>No billing items or tickets found in the last 90 days</p>
<p className="text-sm mt-2">Try a different configuration item or expand the date range</p>
</>
)}
</div>
</CardContent>
</Card>
)}
</>
)}
</main>
</div>
);
}