Add group suggestions feature with API endpoints and frontend integration
ci / build-backend (push) Successful in 1m55s
ci / build-frontend (push) Successful in 44s
ci / tests (push) Successful in 2m8s
ci / sonar (push) Successful in 4m37s

Implemented new API endpoints for suggesting groups based on the current library, allowing admins to retrieve and create groups from suggestions. Updated the backend to include error handling for suggestions and added necessary DTOs. Enhanced the frontend with new API functions and UI components to display suggestions, improving the user experience for group management. Localization updates were made to support new features in both English and Russian.
This commit is contained in:
Leonid Pershin
2026-07-27 04:18:30 +03:00
parent 4774083dc9
commit 24fdbdf680
18 changed files with 810 additions and 1 deletions
@@ -1,6 +1,6 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import { ChevronLeft, GripVertical, Search, Trash2 } from 'lucide-react'
import { ChevronLeft, GripVertical, Search, Sparkles, Trash2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { qk } from '@/shared/api/query-keys'
@@ -51,6 +51,19 @@ export function GroupDetail({ groupId }: Readonly<{ groupId: string }>) {
}
const onError = useApiError()
/**
* Что подходит под сохранённое правило, но в группу ещё не попало: библиотека пополняется после
* того, как группа собрана, и без этой проверки новое шоу лежало бы мимо эфира, пока кто-нибудь
* не вспомнит нажать «Подобрать». Спрашиваем именно сохранённое правило (фильтр не передаём —
* сервер берёт его сам), а не черновик формы: подсказка не должна прыгать, пока крутят поля.
*/
const { data: pending } = useQuery({
queryKey: qk.groups.pending(groupId),
queryFn: () => findGroupCandidates(groupId, null),
enabled: !!group?.filter,
})
const pendingFresh = (pending ?? []).filter((c) => !c.alreadyInGroup)
const saveMutation = useMutation({
mutationFn: () =>
updateGroup(groupId, {
@@ -157,6 +170,29 @@ export function GroupDetail({ groupId }: Readonly<{ groupId: string }>) {
</Badge>
</div>
{pendingFresh.length > 0 && (
<div className="crt-panel flex flex-wrap items-center gap-3 rounded-md border border-primary/40 px-4 py-3 text-sm">
<Sparkles className="h-4 w-4 shrink-0 text-primary" />
<span className="min-w-0 flex-1">
{t('admin.groups.pendingFound', { count: pendingFresh.length })}{' '}
<span className="text-muted-foreground">
{pendingFresh
.slice(0, 3)
.map((c) => c.elementName)
.join(', ')}
{pendingFresh.length > 3 && '…'}
</span>
</span>
<Button
size="sm"
disabled={addMutation.isPending}
onClick={() => addMutation.mutate(pendingFresh)}
>
{t('admin.groups.pendingAdd', { count: pendingFresh.length })}
</Button>
</div>
)}
<div className="grid gap-6 lg:grid-cols-2">
{/* Левая панель — конструктор правила набора */}
<div className="crt-panel flex flex-col gap-4 rounded-md p-4">