188 lines
8.1 KiB
Vue
188 lines
8.1 KiB
Vue
<script setup lang="ts">
|
|
import { onBeforeUnmount, onMounted, watch } from 'vue'
|
|
import { Sparkles, X } from '@lucide/vue'
|
|
|
|
import AdminReviewDecisionPanel from './AdminReviewDecisionPanel.vue'
|
|
import AdminReviewsHistorySection from './AdminReviewsHistorySection.vue'
|
|
import AdminReviewsQueueHeader from './AdminReviewsQueueHeader.vue'
|
|
import AdminReviewsQueueList from './AdminReviewsQueueList.vue'
|
|
import { useAdminReviewsManager } from './useAdminReviewsManager'
|
|
import { watchAdminToast } from '../../composables/useAdminToast'
|
|
|
|
const props = defineProps<{
|
|
open: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
close: []
|
|
}>()
|
|
|
|
const {
|
|
reviewSaving,
|
|
adminMessage,
|
|
adminError,
|
|
reviewForms,
|
|
seasonDetail,
|
|
reviewFilter,
|
|
categoryFilter,
|
|
selectedNominationId,
|
|
candidatePlatformOptions,
|
|
filteredNominations,
|
|
selectedNomination,
|
|
reviewStats,
|
|
reviewedNominations,
|
|
categoryOptions,
|
|
selectedCandidateCollision,
|
|
selectedRelatedPendingNominations,
|
|
selectedNominationSignalSummary,
|
|
canApproveSelected,
|
|
approveNomination,
|
|
rejectNomination,
|
|
selectedPlatformValue,
|
|
handlePlatformSelection,
|
|
extractNominationStreamUrl,
|
|
} = useAdminReviewsManager()
|
|
|
|
watchAdminToast(adminMessage, adminError)
|
|
|
|
function closeModal() {
|
|
emit('close')
|
|
}
|
|
|
|
function onKey(event: KeyboardEvent) {
|
|
if (event.key === 'Escape' && props.open) {
|
|
closeModal()
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => props.open,
|
|
(open) => {
|
|
if (typeof document !== 'undefined') {
|
|
document.body.style.overflow = open ? 'hidden' : ''
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
onMounted(() => window.addEventListener('keydown', onKey))
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('keydown', onKey)
|
|
if (typeof document !== 'undefined') document.body.style.overflow = ''
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="open"
|
|
class="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-6"
|
|
@click.self="closeModal"
|
|
>
|
|
<div class="absolute inset-0 bg-violet-950/35 backdrop-blur-sm" @click="closeModal" />
|
|
|
|
<section class="relative z-10 flex max-h-[calc(100vh-2rem)] w-full max-w-6xl flex-col overflow-hidden rounded-[28px] border border-violet-200/80 bg-white shadow-[0_40px_90px_rgba(76,40,160,0.28)]">
|
|
<header class="flex items-start justify-between gap-4 border-b border-violet-100 bg-[linear-gradient(135deg,#f7f2ff,#fff7ed)] px-5 py-4 sm:px-6">
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
<span class="grid h-9 w-9 shrink-0 place-items-center rounded-2xl bg-violet-100 text-violet-700">
|
|
<Sparkles class="h-4.5 w-4.5" />
|
|
</span>
|
|
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-violet-500">Review-Fokus</p>
|
|
</div>
|
|
<h2 class="mt-3 text-2xl font-bold text-slate-950">Nominierung entscheiden</h2>
|
|
<p class="mt-1 max-w-2xl text-sm leading-6 text-slate-500">
|
|
Die Nominierungsliste bleibt scanbar; Entscheidungen laufen hier konzentriert mit Queue, Prüfung und Kandidatenübernahme.
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="grid h-10 w-10 shrink-0 place-items-center rounded-full text-slate-400 transition hover:bg-white/80 hover:text-violet-700"
|
|
aria-label="Review-Dialog schließen"
|
|
@click="closeModal"
|
|
>
|
|
<X class="h-5 w-5" />
|
|
</button>
|
|
</header>
|
|
|
|
<div class="min-h-0 overflow-y-auto bg-white">
|
|
<AdminReviewsQueueHeader
|
|
v-model:review-filter="reviewFilter"
|
|
v-model:category-filter="categoryFilter"
|
|
:total-pending="seasonDetail.pendingNominations.length"
|
|
:visible-count="filteredNominations.length"
|
|
:review-stats="reviewStats"
|
|
:category-options="categoryOptions"
|
|
/>
|
|
|
|
<div class="space-y-4 p-4 sm:p-6">
|
|
<div class="grid gap-5 xl:grid-cols-[minmax(300px,0.8fr)_minmax(0,1.2fr)]">
|
|
<div class="space-y-2">
|
|
<!-- Keyboard shortcut legend — fixed above scrollable list -->
|
|
<div v-if="filteredNominations.length > 0" class="flex flex-wrap items-center gap-x-4 gap-y-1.5 rounded-2xl border border-violet-100 bg-violet-50/60 px-3 py-2">
|
|
<span class="text-[10px] font-bold uppercase tracking-[0.16em] text-violet-500">Tastaturkürzel</span>
|
|
<span class="flex items-center gap-1.5">
|
|
<span class="inline-flex gap-0.5">
|
|
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-violet-200 bg-white px-1 text-[10px] font-bold text-violet-700 shadow-sm">↑</kbd>
|
|
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-violet-200 bg-white px-1 text-[10px] font-bold text-violet-700 shadow-sm">↓</kbd>
|
|
</span>
|
|
<span class="text-[11px] text-slate-500">/ </span>
|
|
<span class="inline-flex gap-0.5">
|
|
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-violet-200 bg-white px-1 text-[10px] font-bold text-violet-700 shadow-sm">J</kbd>
|
|
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-violet-200 bg-white px-1 text-[10px] font-bold text-violet-700 shadow-sm">K</kbd>
|
|
</span>
|
|
<span class="text-[11px] text-slate-500">Navigieren</span>
|
|
</span>
|
|
<span class="flex items-center gap-1.5">
|
|
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-emerald-200 bg-emerald-50 px-1.5 text-[10px] font-bold text-emerald-700 shadow-sm">A</kbd>
|
|
<span class="text-[11px] text-slate-500">Annehmen</span>
|
|
</span>
|
|
<span class="flex items-center gap-1.5">
|
|
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-rose-200 bg-rose-50 px-1.5 text-[10px] font-bold text-rose-700 shadow-sm">R</kbd>
|
|
<span class="text-[11px] text-slate-500">Ablehnen</span>
|
|
</span>
|
|
</div>
|
|
|
|
<AdminReviewsQueueList
|
|
:nominations="filteredNominations"
|
|
:total-pending="seasonDetail.pendingNominations.length"
|
|
:selected-nomination-id="selectedNominationId"
|
|
@select="selectedNominationId = $event"
|
|
/>
|
|
</div>
|
|
|
|
<AdminReviewDecisionPanel
|
|
:nomination="selectedNomination"
|
|
:review-saving="reviewSaving"
|
|
:review-form="selectedNomination ? reviewForms[selectedNomination.id] : null"
|
|
:candidate-platform-options="candidatePlatformOptions"
|
|
:selected-candidate-collision="selectedCandidateCollision"
|
|
:selected-related-pending-nominations="selectedRelatedPendingNominations"
|
|
:signal-summary="selectedNominationSignalSummary"
|
|
:stream-url="selectedNomination ? extractNominationStreamUrl(selectedNomination) : ''"
|
|
:can-approve-selected="canApproveSelected"
|
|
:selected-platform-value="selectedPlatformValue"
|
|
@platform-change="handlePlatformSelection"
|
|
@approve="approveNomination"
|
|
@reject="rejectNomination"
|
|
/>
|
|
</div>
|
|
|
|
<details class="rounded-[24px] border border-violet-100 bg-violet-50/40">
|
|
<summary class="cursor-pointer px-5 py-4 text-sm font-semibold text-violet-900">
|
|
Letzte Entscheidungen anzeigen
|
|
</summary>
|
|
<div class="border-t border-violet-100 p-5">
|
|
<AdminReviewsHistorySection
|
|
:reviewed-nominations="reviewedNominations"
|
|
:reviewed-total="seasonDetail.reviewedNominations.length"
|
|
/>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|