wulf-pulse/components/quotes/ticket-detail-modal.tsx

268 lines
8.9 KiB
TypeScript
Raw Normal View History

'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>
);
}