sto-k-odnomu/admin/src/components/PackTestsManager.tsx
2026-01-06 23:12:36 +03:00

214 lines
7.6 KiB
TypeScript

import { useState, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
import { testsApi } from '@/api/tests'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Badge } from '@/components/ui/badge'
import { Search, Check } from 'lucide-react'
interface PackTestsManagerProps {
currentTestIds: string[]
onTestsChange: (addIds: string[], removeIds: string[]) => void
disabled?: boolean
}
export function PackTestsManager({
currentTestIds,
onTestsChange,
disabled = false,
}: PackTestsManagerProps) {
const [search, setSearch] = useState('')
const [selectedTests, setSelectedTests] = useState<Set<string>>(new Set())
const [removedTests, setRemovedTests] = useState<Set<string>>(new Set())
// Load all tests with search
const { data: testsData, isLoading } = useQuery({
queryKey: ['tests', 1, 100, search], // Limit to 100 (backend max)
queryFn: () => testsApi.getTests({ page: 1, limit: 100, search }),
enabled: !disabled,
})
// Initialize selected tests from currentTestIds when it changes
useEffect(() => {
const initialSelected = new Set<string>(
currentTestIds.filter((id) => !removedTests.has(id.toString()))
)
setSelectedTests(initialSelected)
// Reset removed tests when currentTestIds changes (e.g., when opening dialog)
setRemovedTests(new Set())
}, [currentTestIds.join(',')]) // Use join to detect array changes
// Calculate which tests to add/remove when selection changes
useEffect(() => {
const currentlySelected = Array.from(selectedTests)
const removed = Array.from(removedTests)
// Tests to add: selected but not in currentTestIds and not removed
const toAdd = currentlySelected.filter(
(id) => !currentTestIds.includes(id) && !removed.includes(id)
)
// Tests to remove: in removedTests
const toRemove = removed.filter((id) => currentTestIds.includes(id))
if (toAdd.length > 0 || toRemove.length > 0) {
onTestsChange(toAdd, toRemove)
} else if (selectedTests.size > 0 || removedTests.size > 0) {
// Also notify if selection was cleared
onTestsChange([], [])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTests.size, removedTests.size, currentTestIds.join(',')])
const handleToggleTest = (testId: string) => {
if (disabled || !testId) return
const testIdStr = String(testId)
const isCurrentlySelected = selectedTests.has(testIdStr) && !removedTests.has(testIdStr)
const isInCurrentPack = currentTestIds.includes(testIdStr)
if (isCurrentlySelected) {
// Deselect test
const newSelected = new Set(selectedTests)
newSelected.delete(testIdStr)
setSelectedTests(newSelected)
// If it was in current pack, mark as removed
if (isInCurrentPack) {
setRemovedTests((prev) => new Set([...prev, testIdStr]))
}
} else {
// Select test
const newSelected = new Set(selectedTests)
newSelected.add(testIdStr)
setSelectedTests(newSelected)
// If it was marked as removed, unmark it
if (removedTests.has(testIdStr)) {
setRemovedTests((prev) => {
const newRemoved = new Set(prev)
newRemoved.delete(testIdStr)
return newRemoved
})
}
}
}
const isTestSelected = (testId: string | number) => {
const testIdStr = String(testId)
return selectedTests.has(testIdStr) && !removedTests.has(testIdStr)
}
const isTestInCurrentPack = (testId: string | number) => {
return currentTestIds.includes(String(testId))
}
const allTests = testsData?.items || []
const filteredTests = search
? allTests.filter(
(test) =>
test.name?.toLowerCase().includes(search.toLowerCase()) ||
test.id?.toLowerCase().includes(search.toLowerCase())
)
: allTests
return (
<div className="space-y-4">
<div className="space-y-2">
<Label>Manage Tests in Pack</Label>
<div className="flex items-center space-x-2">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search tests by name or ID..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="max-w-sm"
disabled={disabled}
/>
</div>
<p className="text-sm text-muted-foreground">
Selected tests: {selectedTests.size - removedTests.size} / {allTests.length}
</p>
</div>
{isLoading ? (
<div className="text-center py-4">Loading tests...</div>
) : (
<div className="border rounded-lg max-h-[400px] overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12"></TableHead>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Questions</TableHead>
<TableHead>Version</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredTests.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground">
No tests found
</TableCell>
</TableRow>
) : (
filteredTests
.filter((test) => test.id) // Only show tests with IDs
.map((test) => {
const testIdStr = String(test.id!)
const selected = isTestSelected(testIdStr)
const inCurrentPack = isTestInCurrentPack(testIdStr)
const newlyRemoved = removedTests.has(testIdStr)
return (
<TableRow
key={test.id}
className={selected ? 'bg-muted/50' : ''}
onClick={() => handleToggleTest(testIdStr)}
>
<TableCell>
{selected ? (
<Check className="h-4 w-4 text-primary" />
) : (
<div className="h-4 w-4 border rounded" />
)}
</TableCell>
<TableCell className="font-mono text-sm">{test.id || '-'}</TableCell>
<TableCell className="font-medium">{test.name}</TableCell>
<TableCell>
{typeof test.questions === 'number'
? test.questions
: test.questions?.length || 0}
</TableCell>
<TableCell>{test.version || 'N/A'}</TableCell>
<TableCell>
{newlyRemoved ? (
<Badge variant="destructive">Removed</Badge>
) : selected && inCurrentPack ? (
<Badge variant="default">In Pack</Badge>
) : selected ? (
<Badge variant="secondary">Selected</Badge>
) : inCurrentPack ? (
<Badge variant="outline">In Pack</Badge>
) : null}
</TableCell>
</TableRow>
)
})
)}
</TableBody>
</Table>
</div>
)}
</div>
)
}