510 lines
20 KiB
TypeScript
510 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>
|
||
|
|
);
|
||
|
|
}
|