Restructure: rename to Pulse and move app to root
- Renamed project from PSA-Utils to Pulse - Moved all app files from autotask-app/ to root - Updated package.json name to 'pulse' - Updated Docker container names to pulse-app and pulse-redis - Updated Docker network name to pulse-network
This commit is contained in:
parent
f429f3af54
commit
3c3124d8c9
117 changed files with 8433 additions and 239 deletions
221
components/tickets/ticket-list.tsx
Normal file
221
components/tickets/ticket-list.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Ticket as TicketType, TicketStatus, Priority } from '@/lib/types/autotask';
|
||||
import { useApi } from '@/lib/hooks/use-api';
|
||||
import { ChevronRight, AlertCircle, Clock, CheckCircle, Ticket } from 'lucide-react';
|
||||
|
||||
interface TicketListProps {
|
||||
resourceId?: number;
|
||||
companyId?: number;
|
||||
}
|
||||
|
||||
export function TicketList({ resourceId, companyId }: TicketListProps) {
|
||||
const [statusLabels, setStatusLabels] = useState<Record<number, string>>({});
|
||||
const [priorityLabels, setPriorityLabels] = useState<Record<number, string>>({});
|
||||
|
||||
let url = '/api/tickets';
|
||||
if (resourceId) url += `?resourceId=${resourceId}`;
|
||||
else if (companyId) url += `?companyId=${companyId}`;
|
||||
|
||||
const { data, loading, error, refetch } = useApi<{ tickets: TicketType[] }>(url);
|
||||
|
||||
// Fetch picklist values for status and priority
|
||||
useEffect(() => {
|
||||
fetch('/api/picklists?entity=Tickets&field=status')
|
||||
.then(res => res.json())
|
||||
.then(data => setStatusLabels(data.picklistValues || {}))
|
||||
.catch(console.error);
|
||||
|
||||
fetch('/api/picklists?entity=Tickets&field=priority')
|
||||
.then(res => res.json())
|
||||
.then(data => setPriorityLabels(data.picklistValues || {}))
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const getStatusBadge = (status: number) => {
|
||||
const label = statusLabels[status] || `Status ${status}`;
|
||||
let variant: 'default' | 'secondary' | 'destructive' | 'outline' = 'default';
|
||||
let icon = null;
|
||||
|
||||
switch (status) {
|
||||
case TicketStatus.New:
|
||||
variant = 'destructive';
|
||||
icon = <AlertCircle className="w-3 h-3 mr-1" />;
|
||||
break;
|
||||
case TicketStatus.InProgress:
|
||||
variant = 'default';
|
||||
icon = <Clock className="w-3 h-3 mr-1" />;
|
||||
break;
|
||||
case TicketStatus.Complete:
|
||||
variant = 'secondary';
|
||||
icon = <CheckCircle className="w-3 h-3 mr-1" />;
|
||||
break;
|
||||
default:
|
||||
variant = 'outline';
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge variant={variant} className="flex items-center">
|
||||
{icon}
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const getPriorityBadge = (priority: number) => {
|
||||
const label = priorityLabels[priority] || `Priority ${priority}`;
|
||||
let variant: 'default' | 'secondary' | 'destructive' | 'outline' = 'default';
|
||||
|
||||
switch (priority) {
|
||||
case Priority.Critical:
|
||||
variant = 'destructive';
|
||||
break;
|
||||
case Priority.High:
|
||||
variant = 'default';
|
||||
break;
|
||||
case Priority.Medium:
|
||||
variant = 'secondary';
|
||||
break;
|
||||
case Priority.Low:
|
||||
variant = 'outline';
|
||||
break;
|
||||
}
|
||||
|
||||
return <Badge variant={variant}>{label}</Badge>;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Tickets</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{[1, 2, 3].map(i => (
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const isConfigError = error.includes('not configured') || error.includes('configuration');
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Tickets</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="text-red-500 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
{isConfigError ? 'API Configuration Required' : `Error loading tickets: ${error}`}
|
||||
</div>
|
||||
{isConfigError && (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
||||
<p className="text-sm mb-3">
|
||||
The Autotask API is not configured. Please set up your credentials to start using the dashboard.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild size="sm">
|
||||
<a href="/setup">Go to Setup</a>
|
||||
</Button>
|
||||
<Button onClick={refetch} variant="outline" size="sm">
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isConfigError && (
|
||||
<Button onClick={refetch} className="mt-4">
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const tickets = data?.tickets || [];
|
||||
|
||||
return (
|
||||
<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 rounded-t-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<Ticket className="w-5 h-5 text-blue-600" />
|
||||
Tickets
|
||||
<Badge variant="secondary" className="ml-2">{tickets.length}</Badge>
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
{tickets.length === 0 ? (
|
||||
<div className="text-muted-foreground text-center py-8">
|
||||
No tickets found
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Number</TableHead>
|
||||
<TableHead>Title</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Priority</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Due</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tickets.map((ticket) => (
|
||||
<TableRow key={ticket.id}>
|
||||
<TableCell className="font-mono">
|
||||
{ticket.ticketNumber}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-md truncate">
|
||||
{ticket.title}
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(ticket.status)}</TableCell>
|
||||
<TableCell>{getPriorityBadge(ticket.priority)}</TableCell>
|
||||
<TableCell>
|
||||
{format(new Date(ticket.createDate), 'MMM d, yyyy')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{ticket.dueDateTime
|
||||
? format(new Date(ticket.dueDateTime), 'MMM d, yyyy')
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="ghost" size="sm">
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue