wulf-pulse/app/admin/data-browser/ticket-notes/page.tsx

211 lines
8 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import DataTable from '@/components/admin/DataTable';
import DetailModal from '@/components/admin/DetailModal';
import { MessageSquare, ArrowLeft, RefreshCw, Filter, Calendar, User } from 'lucide-react';
import Link from 'next/link';
const NOTE_TYPE: Record<number, string> = {
1: 'Task Detail', 2: 'Time Entry Note', 3: 'Ticket Detail',
};
const PUBLISH_MAP: Record<number, { label: string; cls: string }> = {
1: { label: 'All Users', cls: 'bg-green-500/15 text-green-600 border border-green-500/30' },
2: { label: 'Internal', cls: 'bg-amber-500/15 text-amber-700 border border-amber-500/30' },
4: { label: 'Internal Only', cls: 'bg-slate-500/15 text-slate-500 border border-slate-500/30' },
};
export default function TicketNotesPage() {
const [notes, setNotes] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selected, setSelected] = useState<any | null>(null);
const [showModal, setShowModal] = useState(false);
const [totalCount, setTotalCount] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(100);
const [search, setSearch] = useState('');
const [sortBy, setSortBy] = useState('create_date_time');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
const fetchNotes = async (page = 1, overrideSortBy = sortBy, overrideSortOrder = sortOrder) => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams();
if (search) params.append('search', search);
params.append('limit', pageSize.toString());
params.append('offset', ((page - 1) * pageSize).toString());
params.append('sort_by', overrideSortBy);
params.append('sort_order', overrideSortOrder);
const res = await fetch(`/api/data/ticket-notes?${params}`);
if (!res.ok) throw new Error(res.statusText);
const data = await res.json();
setNotes(data.ticketNotes || []);
setTotalCount(data.pagination?.total || 0);
setCurrentPage(page);
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error');
} finally {
setLoading(false);
}
};
useEffect(() => { fetchNotes(1); }, [pageSize]);
const handleSort = (col: string, dir: 'asc' | 'desc') => {
setSortBy(col);
setSortOrder(dir);
fetchNotes(1, col, dir);
};
const columns = [
{
key: 'ticket_number',
label: 'Ticket',
sortable: false,
render: (v: string) => v
? <Badge variant="outline" className="font-mono text-xs">{v}</Badge>
: <span className="text-muted-foreground"></span>,
},
{
key: 'create_date_time',
label: 'Date',
sortable: true,
render: (v: string) => v
? <span className="inline-flex items-center gap-1 text-xs"><Calendar className="w-3 h-3" />{new Date(v).toLocaleDateString()}</span>
: <span className="text-muted-foreground"></span>,
},
{
key: 'creator_name',
label: 'Creator',
sortable: false,
render: (v: string) => v
? <span className="inline-flex items-center gap-1 text-xs"><User className="w-3 h-3" />{v}</span>
: <span className="text-muted-foreground"></span>,
},
{
key: 'publish',
label: 'Visibility',
sortable: true,
render: (v: number) => {
const p = PUBLISH_MAP[v];
return p
? <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${p.cls}`}>{p.label}</span>
: <span className="text-muted-foreground text-xs">{v}</span>;
},
},
{
key: 'note_type',
label: 'Type',
sortable: true,
render: (v: number) => <span className="text-xs text-muted-foreground">{NOTE_TYPE[v] ?? `Type ${v}`}</span>,
},
{
key: 'title',
label: 'Title',
sortable: true,
render: (v: string) => <div className="max-w-xs truncate text-sm" title={v}>{v || <span className="text-muted-foreground italic">No title</span>}</div>,
},
{
key: 'description',
label: 'Preview',
sortable: false,
render: (v: string) => <div className="max-w-sm truncate text-xs text-muted-foreground" title={v}>{v || '—'}</div>,
},
];
return (
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link href="/admin/data-browser">
<Button variant="outline" size="sm" className="gap-2">
<ArrowLeft className="w-4 h-4" />
<span className="hidden sm:inline">Back</span>
</Button>
</Link>
<MessageSquare className="w-8 h-8 text-blue-600" />
<div>
<h1 className="text-3xl font-bold">Ticket Notes</h1>
<p className="text-muted-foreground">Browse notes attached to tickets</p>
</div>
</div>
<Button variant="outline" onClick={() => fetchNotes(currentPage)} disabled={loading}>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2"><Filter className="w-5 h-5" />Filters</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label>Search title / description</Label>
<Input placeholder="Search..." value={search} onChange={e => setSearch(e.target.value)}
onKeyDown={e => e.key === 'Enter' && fetchNotes(1)} />
</div>
<div className="space-y-2">
<Label>Page Size</Label>
<Select value={pageSize.toString()} onValueChange={v => setPageSize(parseInt(v))}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
{['50','100','250','500'].map(v => <SelectItem key={v} value={v}>{v}</SelectItem>)}
</SelectContent>
</Select>
</div>
<div className="flex items-end gap-2">
<Button onClick={() => fetchNotes(1)} disabled={loading}>Apply</Button>
<Button variant="outline" onClick={() => { setSearch(''); fetchNotes(1); }}>Clear</Button>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Ticket Notes ({totalCount.toLocaleString()} total)</CardTitle>
<CardDescription>Showing {notes.length} of {totalCount.toLocaleString()} Click a row to view details</CardDescription>
</CardHeader>
<CardContent>
{error && (
<div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-md p-4 mb-4">
<p className="text-red-800 dark:text-red-200">{error}</p>
</div>
)}
<DataTable
data={notes}
columns={columns}
isLoading={loading}
onRowClick={row => { setSelected(row); setShowModal(true); }}
totalCount={totalCount}
page={currentPage}
pageSize={pageSize}
onPageChange={p => fetchNotes(p)}
onSort={handleSort}
/>
</CardContent>
</Card>
<DetailModal
open={showModal}
onOpenChange={setShowModal}
title={selected?.title || `Note #${selected?.id}`}
data={selected}
/>
</div>
);
}