diff --git a/frontend/src/components/tickets/TicketFilterBar.tsx b/frontend/src/components/tickets/TicketFilterBar.tsx new file mode 100644 index 00000000..3abea58d --- /dev/null +++ b/frontend/src/components/tickets/TicketFilterBar.tsx @@ -0,0 +1,150 @@ +// frontend/src/components/tickets/TicketFilterBar.tsx +import { Search, X } from 'lucide-react' +import { cn } from '@/lib/utils' +import type { TicketFilters, PSAPriority } from '@/types/tickets' +import type { PSABoard, PSATicketStatusItem } from '@/types/integrations' + +interface TicketFilterBarProps { + filters: TicketFilters + onChange: (updated: Partial) => void + boards: PSABoard[] + statuses: PSATicketStatusItem[] + priorities: PSAPriority[] + members: { id: number; name: string }[] + total: number + page: number + pageSize: number + onPageChange: (page: number) => void + loading: boolean +} + +export function TicketFilterBar({ + filters, onChange, boards, statuses, priorities, members, + total, page, pageSize, onPageChange, loading, +}: TicketFilterBarProps) { + const start = (page - 1) * pageSize + 1 + const end = Math.min(page * pageSize, total) + const hasNext = page * pageSize < total + const hasPrev = page > 1 + + return ( +
+ {/* Filter row */} +
+ {/* Search */} +
+ + onChange({ search: e.target.value })} + /> +
+ + {/* Assignment */} + + + {/* Board */} + + + {/* Status */} + + + {/* Priority */} + + + {/* Include closed */} + + + {/* Clear filters */} + {(filters.search || filters.board_id || filters.status_id || filters.priority || filters.assigned !== 'all' || filters.include_closed) && ( + + )} +
+ + {/* Pagination row */} + {total > 0 && ( +
+ + {loading ? 'Loading…' : `Showing ${start}–${end} of ${total} tickets`} + +
+ + +
+
+ )} +
+ ) +} diff --git a/frontend/src/components/tickets/TicketListRow.tsx b/frontend/src/components/tickets/TicketListRow.tsx new file mode 100644 index 00000000..145b9065 --- /dev/null +++ b/frontend/src/components/tickets/TicketListRow.tsx @@ -0,0 +1,72 @@ +// frontend/src/components/tickets/TicketListRow.tsx +import { cn } from '@/lib/utils' +import type { PSATicketSearchResult } from '@/types/integrations' + +interface TicketListRowProps { + ticket: PSATicketSearchResult + selected: boolean + onClick: () => void +} + +const PRIORITY_STYLES: Record = { + Critical: 'text-danger', + High: 'text-danger', + Medium: 'text-warning', + Low: 'text-muted-foreground', +} + +const STATUS_STYLES: Record = { + New: { bg: 'bg-accent/10', text: 'text-accent' }, + 'In Progress': { bg: 'bg-warning/10', text: 'text-warning' }, + Waiting: { bg: 'bg-success/10', text: 'text-success' }, + Resolved: { bg: 'bg-elevated/50', text: 'text-muted-foreground' }, +} + +function statusStyle(name: string | null) { + if (!name) return { bg: 'bg-elevated', text: 'text-muted-foreground' } + return STATUS_STYLES[name] ?? { bg: 'bg-elevated', text: 'text-muted-foreground' } +} + +export function TicketListRow({ ticket, selected, onClick }: TicketListRowProps) { + const { bg, text } = statusStyle(ticket.status_name) + const priorityClass = PRIORITY_STYLES[ticket.priority_name ?? ''] ?? 'text-muted-foreground' + + return ( +
e.key === 'Enter' && onClick()} + className={cn( + 'flex items-center gap-3 px-4 py-2.5 cursor-pointer transition-colors border-b border-default text-sm', + selected ? 'bg-accent/5' : 'hover:bg-elevated' + )} + > + {/* ID */} + #{ticket.id} + + {/* Summary */} + {ticket.summary} + + {/* Company */} + + {ticket.company_name ?? '—'} + + + {/* Board */} + + {ticket.board_name ?? '—'} + + + {/* Status badge */} + + {ticket.status_name ?? '—'} + + + {/* Priority */} + + {ticket.priority_name ?? '—'} + +
+ ) +}