268 lines
8.9 KiB
TypeScript
268 lines
8.9 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState, useEffect } from 'react';
|
||
|
|
import {
|
||
|
|
Dialog,
|
||
|
|
DialogContent,
|
||
|
|
DialogDescription,
|
||
|
|
DialogHeader,
|
||
|
|
DialogTitle,
|
||
|
|
} from '@/components/ui/dialog';
|
||
|
|
import { Badge } from '@/components/ui/badge';
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import { Separator } from '@/components/ui/separator';
|
||
|
|
import {
|
||
|
|
Ticket as TicketIcon,
|
||
|
|
Building2,
|
||
|
|
User,
|
||
|
|
Calendar,
|
||
|
|
Clock,
|
||
|
|
AlertCircle,
|
||
|
|
CheckCircle,
|
||
|
|
ExternalLink,
|
||
|
|
Loader2
|
||
|
|
} from 'lucide-react';
|
||
|
|
|
||
|
|
interface TicketDetailModalProps {
|
||
|
|
ticketNumber: string;
|
||
|
|
open: boolean;
|
||
|
|
onOpenChange: (open: boolean) => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDetailModalProps) {
|
||
|
|
const [ticket, setTicket] = useState<any>(null);
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
const fetchTicketDetails = async () => {
|
||
|
|
setLoading(true);
|
||
|
|
setError(null);
|
||
|
|
setTicket(null);
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/tickets');
|
||
|
|
if (response.ok) {
|
||
|
|
const data = await response.json();
|
||
|
|
const matchingTicket = data.tickets?.find((t: any) => t.ticketNumber === ticketNumber);
|
||
|
|
|
||
|
|
if (matchingTicket) {
|
||
|
|
const detailResponse = await fetch(`/api/tickets/${matchingTicket.id}`);
|
||
|
|
if (detailResponse.ok) {
|
||
|
|
const detailData = await detailResponse.json();
|
||
|
|
setTicket(detailData.ticket);
|
||
|
|
} else {
|
||
|
|
setError('Failed to fetch ticket details');
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
setError(`Ticket ${ticketNumber} not found`);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
setError('Failed to fetch tickets list');
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
setError('Failed to fetch ticket');
|
||
|
|
console.error('Error fetching ticket:', err);
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Fetch ticket when modal opens
|
||
|
|
useEffect(() => {
|
||
|
|
if (open && ticketNumber) {
|
||
|
|
fetchTicketDetails();
|
||
|
|
}
|
||
|
|
}, [open, ticketNumber]);
|
||
|
|
|
||
|
|
const handleOpenChange = (newOpen: boolean) => {
|
||
|
|
onOpenChange(newOpen);
|
||
|
|
};
|
||
|
|
|
||
|
|
const getStatusBadge = (status: number) => {
|
||
|
|
const statusMap: Record<number, { label: string; variant: "default" | "secondary" | "destructive" | "outline" }> = {
|
||
|
|
1: { label: "New", variant: "default" },
|
||
|
|
5: { label: "Complete", variant: "secondary" },
|
||
|
|
8: { label: "In Progress", variant: "default" },
|
||
|
|
};
|
||
|
|
const config = statusMap[status] || { label: `Status ${status}`, variant: "outline" };
|
||
|
|
return <Badge variant={config.variant}>{config.label}</Badge>;
|
||
|
|
};
|
||
|
|
|
||
|
|
const getPriorityBadge = (priority: number) => {
|
||
|
|
const priorityMap: Record<number, { label: string; variant: "default" | "secondary" | "destructive" }> = {
|
||
|
|
1: { label: "Critical", variant: "destructive" },
|
||
|
|
2: { label: "High", variant: "destructive" },
|
||
|
|
3: { label: "Medium", variant: "default" },
|
||
|
|
4: { label: "Low", variant: "secondary" },
|
||
|
|
};
|
||
|
|
const config = priorityMap[priority] || { label: `Priority ${priority}`, variant: "outline" as any };
|
||
|
|
return <Badge variant={config.variant}>{config.label}</Badge>;
|
||
|
|
};
|
||
|
|
|
||
|
|
const formatDate = (dateString?: string) => {
|
||
|
|
if (!dateString) return 'N/A';
|
||
|
|
return new Date(dateString).toLocaleString('en-US', {
|
||
|
|
year: 'numeric',
|
||
|
|
month: 'short',
|
||
|
|
day: 'numeric',
|
||
|
|
hour: '2-digit',
|
||
|
|
minute: '2-digit',
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||
|
|
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle className="flex items-center gap-2">
|
||
|
|
<TicketIcon className="h-5 w-5" />
|
||
|
|
Ticket Details
|
||
|
|
</DialogTitle>
|
||
|
|
<DialogDescription>
|
||
|
|
Viewing ticket information from Autotask PSA
|
||
|
|
</DialogDescription>
|
||
|
|
</DialogHeader>
|
||
|
|
|
||
|
|
{loading && (
|
||
|
|
<div className="flex items-center justify-center py-8">
|
||
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{error && (
|
||
|
|
<div className="flex items-center gap-2 p-4 bg-destructive/10 text-destructive rounded-lg">
|
||
|
|
<AlertCircle className="h-5 w-5" />
|
||
|
|
<span>{error}</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{ticket && !loading && (
|
||
|
|
<div className="space-y-4">
|
||
|
|
{/* Header Info */}
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-muted rounded-lg">
|
||
|
|
<div>
|
||
|
|
<p className="text-sm text-muted-foreground">Ticket Number</p>
|
||
|
|
<p className="font-mono font-semibold text-blue-600 dark:text-blue-400">
|
||
|
|
{ticket.ticketNumber}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<p className="text-sm text-muted-foreground">Status</p>
|
||
|
|
<div className="mt-1">{getStatusBadge(ticket.status)}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Title */}
|
||
|
|
<div>
|
||
|
|
<h3 className="text-lg font-semibold">{ticket.title}</h3>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Key Details */}
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
<div className="space-y-3">
|
||
|
|
<div>
|
||
|
|
<p className="text-sm text-muted-foreground flex items-center gap-2">
|
||
|
|
<User className="h-4 w-4" />
|
||
|
|
Assigned To
|
||
|
|
</p>
|
||
|
|
<p className="font-medium">{ticket.assignedResourceName || 'Unassigned'}</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<p className="text-sm text-muted-foreground">Priority</p>
|
||
|
|
<div className="mt-1">{getPriorityBadge(ticket.priority)}</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<p className="text-sm text-muted-foreground flex items-center gap-2">
|
||
|
|
<Calendar className="h-4 w-4" />
|
||
|
|
Created Date
|
||
|
|
</p>
|
||
|
|
<p className="font-medium">{formatDate(ticket.createDate)}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-3">
|
||
|
|
<div>
|
||
|
|
<p className="text-sm text-muted-foreground flex items-center gap-2">
|
||
|
|
<Clock className="h-4 w-4" />
|
||
|
|
Due Date
|
||
|
|
</p>
|
||
|
|
<p className="font-medium">{formatDate(ticket.dueDateTime)}</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{ticket.resolvedDateTime && (
|
||
|
|
<div>
|
||
|
|
<p className="text-sm text-muted-foreground flex items-center gap-2">
|
||
|
|
<CheckCircle className="h-4 w-4" />
|
||
|
|
Resolved Date
|
||
|
|
</p>
|
||
|
|
<p className="font-medium">{formatDate(ticket.resolvedDateTime)}</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{ticket.purchaseOrderNumber && (
|
||
|
|
<div>
|
||
|
|
<p className="text-sm text-muted-foreground">PO Number</p>
|
||
|
|
<p className="font-medium">{ticket.purchaseOrderNumber}</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Separator />
|
||
|
|
|
||
|
|
{/* Description */}
|
||
|
|
{ticket.description && (
|
||
|
|
<div>
|
||
|
|
<p className="text-sm font-semibold mb-2">Description</p>
|
||
|
|
<div className="p-3 bg-muted rounded-lg whitespace-pre-wrap text-sm">
|
||
|
|
{ticket.description}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Resolution */}
|
||
|
|
{ticket.resolution && (
|
||
|
|
<div>
|
||
|
|
<p className="text-sm font-semibold mb-2">Resolution</p>
|
||
|
|
<div className="p-3 bg-muted rounded-lg whitespace-pre-wrap text-sm">
|
||
|
|
{ticket.resolution}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Quote Number */}
|
||
|
|
{ticket.userDefinedFields && (
|
||
|
|
<>
|
||
|
|
{ticket.userDefinedFields.find((f: any) => f.name === 'Quote Number' && f.value) && (
|
||
|
|
<div>
|
||
|
|
<p className="text-sm text-muted-foreground">Quote Number</p>
|
||
|
|
<p className="font-medium">
|
||
|
|
{ticket.userDefinedFields.find((f: any) => f.name === 'Quote Number')?.value}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Actions */}
|
||
|
|
<div className="flex justify-end gap-2 pt-4">
|
||
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||
|
|
Close
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
onClick={() => {
|
||
|
|
window.open(`https://wam.autotask.net/Mvc/ServiceDesk/TicketDetail.mvc?ticketId=${ticket.id}`, '_blank');
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<ExternalLink className="h-4 w-4 mr-2" />
|
||
|
|
Open in Autotask
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
);
|
||
|
|
}
|