feat(ticket): add global ticket assignment page
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
import { MultiSelectPopover } from '@/components/form/MultiSelectPopover';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useDebounce } from '@/hooks/useDebounce';
|
||||
|
||||
import { useTicketDetectionsQuery } from '../../../hooks/useTicketQueries';
|
||||
import { useIssueColumns } from './IssueColumns';
|
||||
import { IssueTable } from './IssueTable';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
interface IssueTypeOption {
|
||||
class_name: string;
|
||||
display_name: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface ChooseIssuesSectionProps {
|
||||
videoId?: string;
|
||||
issueTypes: IssueTypeOption[];
|
||||
}
|
||||
|
||||
export function ChooseIssuesSection({
|
||||
videoId,
|
||||
issueTypes,
|
||||
}: ChooseIssuesSectionProps) {
|
||||
const [selectedIssueTypes, setSelectedIssueTypes] = useState<string[]>([]);
|
||||
const [detectionSearch, setDetectionSearch] = useState('');
|
||||
const [skip, setSkip] = useState(0);
|
||||
const [limit, setLimit] = useState(DEFAULT_PAGE_SIZE);
|
||||
const debouncedSearch = useDebounce(detectionSearch.trim(), 350);
|
||||
const columns = useIssueColumns();
|
||||
const issueTypeOptions = useMemo(
|
||||
() =>
|
||||
issueTypes.map((issue) => ({
|
||||
value: issue.class_name,
|
||||
label: `${issue.display_name} (${issue.count})`,
|
||||
})),
|
||||
[issueTypes],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSkip(0);
|
||||
}, [debouncedSearch, selectedIssueTypes]);
|
||||
|
||||
const queryParams = useMemo(
|
||||
() => ({
|
||||
skip,
|
||||
limit,
|
||||
class_name:
|
||||
selectedIssueTypes.length > 0 ? selectedIssueTypes : undefined,
|
||||
search: debouncedSearch || undefined,
|
||||
sort: 'timestamp_asc',
|
||||
}),
|
||||
[debouncedSearch, limit, selectedIssueTypes, skip],
|
||||
);
|
||||
const detectionsQuery = useTicketDetectionsQuery(videoId, queryParams);
|
||||
const result = detectionsQuery.data;
|
||||
|
||||
return (
|
||||
<IssueTable
|
||||
columns={columns}
|
||||
issues={result?.items ?? []}
|
||||
isLoading={detectionsQuery.isLoading || detectionsQuery.isFetching}
|
||||
isError={detectionsQuery.isError}
|
||||
toolbar={
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<div className="w-fit">
|
||||
<MultiSelectPopover
|
||||
label="Issue Types"
|
||||
options={issueTypeOptions}
|
||||
values={selectedIssueTypes}
|
||||
onValuesChange={setSelectedIssueTypes}
|
||||
searchPlaceholder="Search issue types"
|
||||
emptyMessage="No issue types found."
|
||||
align="start"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-full sm:ml-auto sm:w-72">
|
||||
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={detectionSearch}
|
||||
onChange={(event) => setDetectionSearch(event.target.value)}
|
||||
placeholder="Search by detection ID"
|
||||
aria-label="Search by detection ID"
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
skip={skip}
|
||||
limit={limit}
|
||||
total={result?.total ?? 0}
|
||||
onPageChange={setSkip}
|
||||
onLimitChange={setLimit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user