Added a new endpoint for adding shows to already assembled franchises based on the same suggestion key. Updated the CollectionSuggestion and CollectionSuggestionDto records to include a CollectionId for tracking existing collections. Enhanced the CollectionSuggestions component in the frontend to support this new functionality, allowing users to add missing parts to existing collections. Updated localization strings to reflect the new addition feature in both English and Russian, ensuring clarity for users. Adjusted tests to verify the correct behavior of the addition process and its integration with existing collection logic.
163 lines
7.4 KiB
TypeScript
163 lines
7.4 KiB
TypeScript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { ChevronDown, ChevronRight, Plus, Wand2 } from 'lucide-react'
|
|
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { qk } from '@/shared/api/query-keys'
|
|
import type { CollectionSuggestionDto } from '@/shared/api/types'
|
|
import { useApiError } from '@/shared/lib/use-api-error'
|
|
import { Badge } from '@/shared/ui/badge'
|
|
import { Button } from '@/shared/ui/button'
|
|
import { toast } from '@/shared/ui/toast-store'
|
|
import { addShowsFromSuggestion, createCollectionFromSuggestion, suggestCollections } from './api'
|
|
|
|
/**
|
|
* Что стоит собрать во франшизу. Состав виден целиком до создания — по названиям иногда
|
|
* промахивается, и проверить глазами должно быть проще, чем разбирать потом.
|
|
*
|
|
* Уже созданное по умолчанию скрыто: строка без кнопки — шум. Счётчик скрытого остаётся видимым,
|
|
* чтобы предложение не выглядело пропавшим бесследно.
|
|
*/
|
|
export function CollectionSuggestions() {
|
|
const { t } = useTranslation()
|
|
const queryClient = useQueryClient()
|
|
const onError = useApiError()
|
|
// Свёрнуто по умолчанию: предложения — подсказка, а не главное на экране, и развёрнутый
|
|
// список из десятка строк отодвигал бы сам справочник за нижний край.
|
|
const [collapsed, setCollapsed] = useState(true)
|
|
const [showExisting, setShowExisting] = useState(false)
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: qk.collections.suggestions,
|
|
queryFn: suggestCollections,
|
|
})
|
|
|
|
const invalidate = () => {
|
|
void queryClient.invalidateQueries({ queryKey: qk.collections.all })
|
|
void queryClient.invalidateQueries({ queryKey: qk.collections.suggestions })
|
|
void queryClient.invalidateQueries({ queryKey: qk.shows.all })
|
|
}
|
|
|
|
const create = useMutation({
|
|
mutationFn: (suggestion: CollectionSuggestionDto) =>
|
|
createCollectionFromSuggestion(suggestion.key),
|
|
onSuccess: () => {
|
|
toast.success(t('admin.collections.suggestions.created'))
|
|
invalidate()
|
|
},
|
|
onError,
|
|
})
|
|
|
|
/** Дополнение уже собранной франшизы: коллекцию сервер находит по тому же ключу предложения. */
|
|
const add = useMutation({
|
|
mutationFn: (suggestion: CollectionSuggestionDto) => addShowsFromSuggestion(suggestion.key),
|
|
onSuccess: (added) => {
|
|
toast.success(t('admin.collections.suggestions.added', { count: added }))
|
|
invalidate()
|
|
},
|
|
onError,
|
|
})
|
|
|
|
if (isLoading || !data || data.length === 0) return null
|
|
|
|
const fresh = data.filter((s) => !s.alreadyExists)
|
|
const existing = data.length - fresh.length
|
|
const visible = showExisting ? data : fresh
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<button
|
|
type="button"
|
|
className="flex items-center gap-2 self-start text-sm font-semibold uppercase tracking-wide text-muted-foreground hover:text-foreground"
|
|
onClick={() => setCollapsed((v) => !v)}
|
|
>
|
|
{collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
|
<Wand2 className="h-4 w-4" />
|
|
{t('admin.collections.suggestions.title')}
|
|
<Badge variant="muted">{fresh.length}</Badge>
|
|
</button>
|
|
|
|
{!collapsed && (
|
|
<>
|
|
<p className="text-xs text-muted-foreground">{t('admin.collections.suggestions.hint')}</p>
|
|
{visible.length === 0 && (
|
|
<p className="text-sm text-muted-foreground">
|
|
{t('admin.collections.suggestions.allCreated')}
|
|
</p>
|
|
)}
|
|
<ul
|
|
className={`crt-panel divide-y divide-border rounded-md text-sm ${
|
|
visible.length === 0 ? 'hidden' : ''
|
|
}`}
|
|
>
|
|
{visible.map((suggestion) => (
|
|
<li key={suggestion.key} className="flex flex-col gap-1 px-4 py-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="min-w-0 flex-1 truncate font-medium">{suggestion.name}</span>
|
|
{/* Дополнение и создание в одном списке: без пометки строки неотличимы, а
|
|
действия у них разные — одна заводит франшизу, другая правит собранную. */}
|
|
{suggestion.collectionId && (
|
|
<Badge variant="muted">
|
|
{t('admin.collections.suggestions.additionBadge')}
|
|
</Badge>
|
|
)}
|
|
<Badge variant={suggestion.source === 'Metadata' ? 'default' : 'muted'}>
|
|
{t(`admin.collections.suggestions.sources.${suggestion.source}`)}
|
|
</Badge>
|
|
<span className="text-muted-foreground">
|
|
{t('admin.collections.suggestions.parts', { count: suggestion.parts.length })}
|
|
</span>
|
|
{suggestion.collectionId ? (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={add.isPending}
|
|
onClick={() => add.mutate(suggestion)}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
{t('admin.collections.suggestions.add')}
|
|
</Button>
|
|
) : (
|
|
<>
|
|
{suggestion.alreadyExists ? (
|
|
<Badge variant="muted">{t('admin.collections.suggestions.exists')}</Badge>
|
|
) : (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={create.isPending}
|
|
onClick={() => create.mutate(suggestion)}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
{t('common.create')}
|
|
</Button>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
{/* Состав в предлагаемом порядке: по нему и видно, промахнулась ли догадка.
|
|
У дополнения это только недостающие части — их и добавит кнопка. */}
|
|
<p className="text-xs text-muted-foreground">
|
|
{suggestion.parts
|
|
.map((part) => (part.year ? `${part.name} (${part.year})` : part.name))
|
|
.join(' → ')}
|
|
</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{existing > 0 && (
|
|
<button
|
|
type="button"
|
|
className="self-start text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
|
onClick={() => setShowExisting((v) => !v)}
|
|
>
|
|
{showExisting
|
|
? t('admin.collections.suggestions.hideExisting')
|
|
: t('admin.collections.suggestions.showExisting', { count: existing })}
|
|
</button>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|