fix: ticket modal — picklist labels for status/priority, compact layout
- Ticket [id] route: resolve statusLabel/priorityLabel via getTicketStatusPicklist() and getTicketPriorityPicklist() with module-level cache (fetched once per deploy) - Modal: remove subtitle, move ticket# and title into header, compact single-row metadata (status badge + priority badge + assigned resource), dates in 2-col grid, smaller buttons
This commit is contained in:
parent
115819bea0
commit
0a318fb9d6
2 changed files with 89 additions and 120 deletions
|
|
@ -4,7 +4,6 @@ import { useState, useEffect } from 'react';
|
|||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
|
|
@ -13,7 +12,6 @@ import { Button } from '@/components/ui/button';
|
|||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Ticket as TicketIcon,
|
||||
Building2,
|
||||
User,
|
||||
Calendar,
|
||||
Clock,
|
||||
|
|
@ -77,25 +75,24 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
|
|||
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 getStatusBadge = (status: number, label?: string | null) => {
|
||||
const resolved = label || `Status ${status}`;
|
||||
const lower = resolved.toLowerCase();
|
||||
const variant =
|
||||
lower.includes('complete') || lower.includes('closed') || lower.includes('resolved') ? 'secondary' :
|
||||
lower.includes('progress') || lower.includes('waiting') ? 'default' :
|
||||
lower.includes('new') ? 'default' : 'outline';
|
||||
return <Badge variant={variant}>{resolved}</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 getPriorityBadge = (priority: number, label?: string | null) => {
|
||||
const resolved = label || `Priority ${priority}`;
|
||||
const lower = resolved.toLowerCase();
|
||||
const variant: "destructive" | "default" | "secondary" | "outline" =
|
||||
lower.includes('critical') || lower.includes('high') ? 'destructive' :
|
||||
lower.includes('medium') ? 'default' :
|
||||
lower.includes('low') ? 'secondary' : 'outline';
|
||||
return <Badge variant={variant}>{resolved}</Badge>;
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string) => {
|
||||
|
|
@ -111,102 +108,69 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
|
|||
|
||||
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
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader className="pb-0">
|
||||
<DialogTitle className="flex items-center gap-2 text-base">
|
||||
<TicketIcon className="h-4 w-4 shrink-0" />
|
||||
<span className="font-mono text-blue-600 dark:text-blue-400">{ticketNumber}</span>
|
||||
{ticket && <span className="text-muted-foreground font-normal truncate">{ticket.title}</span>}
|
||||
</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 className="flex items-center justify-center py-6">
|
||||
<Loader2 className="h-6 w-6 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" />
|
||||
<div className="flex items-center gap-2 p-3 bg-destructive/10 text-destructive rounded-lg text-sm">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
<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 className="space-y-3">
|
||||
{/* Metadata row */}
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm">
|
||||
{getStatusBadge(ticket.status, ticket.statusLabel)}
|
||||
{getPriorityBadge(ticket.priority, ticket.priorityLabel)}
|
||||
{ticket.assignedResourceName && (
|
||||
<span className="flex items-center gap-1 text-muted-foreground">
|
||||
<User className="h-3 w-3" />
|
||||
{ticket.assignedResourceName}
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
{/* Dates row */}
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Calendar className="h-3 w-3 shrink-0" />
|
||||
<span>Created</span>
|
||||
<span className="text-foreground font-medium">{formatDate(ticket.createDate)}</span>
|
||||
</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>
|
||||
{ticket.dueDateTime && (
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Clock className="h-3 w-3 shrink-0" />
|
||||
<span>Due</span>
|
||||
<span className="text-foreground font-medium">{formatDate(ticket.dueDateTime)}</span>
|
||||
</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>
|
||||
)}
|
||||
{ticket.resolvedDateTime && (
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<CheckCircle className="h-3 w-3 shrink-0" />
|
||||
<span>Resolved</span>
|
||||
<span className="text-foreground font-medium">{formatDate(ticket.resolvedDateTime)}</span>
|
||||
</div>
|
||||
)}
|
||||
{ticket.purchaseOrderNumber && (
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<span>PO</span>
|
||||
<span className="text-foreground font-medium">{ticket.purchaseOrderNumber}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
|
@ -214,7 +178,7 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
|
|||
{/* Description */}
|
||||
{ticket.description && (
|
||||
<div>
|
||||
<p className="text-sm font-semibold mb-2">Description</p>
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1">Description</p>
|
||||
<div className="p-3 bg-muted rounded-lg whitespace-pre-wrap text-sm">
|
||||
{ticket.description}
|
||||
</div>
|
||||
|
|
@ -224,33 +188,20 @@ export function TicketDetailModal({ ticketNumber, open, onOpenChange }: TicketDe
|
|||
{/* Resolution */}
|
||||
{ticket.resolution && (
|
||||
<div>
|
||||
<p className="text-sm font-semibold mb-2">Resolution</p>
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-1">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)}>
|
||||
<div className="flex justify-end gap-2 pt-1">
|
||||
<Button variant="outline" size="sm" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
window.open(`https://wam.autotask.net/Mvc/ServiceDesk/TicketDetail.mvc?ticketId=${ticket.id}`, '_blank');
|
||||
}}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue