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
|
|
@ -1,6 +1,20 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAutotaskClient } from '@/lib/services/autotask-factory';
|
||||
|
||||
let _statusPicklist: Record<number, string> | null = null;
|
||||
let _priorityPicklist: Record<number, string> | null = null;
|
||||
|
||||
async function getPicklists() {
|
||||
const client = getAutotaskClient();
|
||||
if (!_statusPicklist) {
|
||||
try { _statusPicklist = await client.getTicketStatusPicklist(); } catch { _statusPicklist = {}; }
|
||||
}
|
||||
if (!_priorityPicklist) {
|
||||
try { _priorityPicklist = await client.getTicketPriorityPicklist(); } catch { _priorityPicklist = {}; }
|
||||
}
|
||||
return { statusPicklist: _statusPicklist!, priorityPicklist: _priorityPicklist! };
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
|
|
@ -9,8 +23,10 @@ export async function GET(
|
|||
const { id } = await params;
|
||||
const client = getAutotaskClient();
|
||||
|
||||
// Get ticket by ID
|
||||
const ticket = await client.getEntityById('Tickets', parseInt(id));
|
||||
const [ticket, { statusPicklist, priorityPicklist }] = await Promise.all([
|
||||
client.getEntityById('Tickets', parseInt(id)),
|
||||
getPicklists(),
|
||||
]);
|
||||
|
||||
if (!ticket) {
|
||||
return NextResponse.json(
|
||||
|
|
@ -19,9 +35,9 @@ export async function GET(
|
|||
);
|
||||
}
|
||||
|
||||
// Get assigned resource name if available
|
||||
let assignedResourceName = null;
|
||||
const ticketData = ticket as any;
|
||||
|
||||
let assignedResourceName = null;
|
||||
if (ticketData.assignedResourceID) {
|
||||
try {
|
||||
const resource = await client.getEntityById('Resources', ticketData.assignedResourceID) as any;
|
||||
|
|
@ -35,6 +51,8 @@ export async function GET(
|
|||
ticket: {
|
||||
...ticket,
|
||||
assignedResourceName,
|
||||
statusLabel: statusPicklist[ticketData.status] ?? null,
|
||||
priorityLabel: priorityPicklist[ticketData.priority] ?? null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -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