Refine admin workflows and team access
This commit is contained in:
@@ -4,9 +4,12 @@ import { CheckCircle2, ExternalLink, Film, Search, Trash2, TriangleAlert, Undo2,
|
||||
import AdminPageHeader from '../../components/admin/AdminPageHeader.vue'
|
||||
import AdminSeasonToolbar from '../../components/admin/AdminSeasonToolbar.vue'
|
||||
import { useAdminClipManager } from '../../components/admin/useAdminClipManager'
|
||||
import { watchAdminToast } from '../../composables/useAdminToast'
|
||||
import Button from '../../components/ui/Button.vue'
|
||||
import Card from '../../components/ui/Card.vue'
|
||||
import Modal from '../../components/ui/Modal.vue'
|
||||
import NativeSelect from '../../components/ui/NativeSelect.vue'
|
||||
import PaginationFooter from '../../components/ui/PaginationFooter.vue'
|
||||
|
||||
const {
|
||||
query,
|
||||
@@ -15,6 +18,7 @@ const {
|
||||
categoryFilter,
|
||||
deleting,
|
||||
statusSaving,
|
||||
bulkSaving,
|
||||
adminMessage,
|
||||
adminError,
|
||||
clipToDelete,
|
||||
@@ -22,6 +26,11 @@ const {
|
||||
submissions,
|
||||
categoryName,
|
||||
clips,
|
||||
page,
|
||||
totalPages,
|
||||
pagedClips,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
clipEmbeds,
|
||||
stats,
|
||||
statusFilters,
|
||||
@@ -33,8 +42,11 @@ const {
|
||||
duplicateUrlCount,
|
||||
creatorClipCount,
|
||||
updateClipStatus,
|
||||
bulkUpdateStatus,
|
||||
confirmDelete,
|
||||
} = useAdminClipManager()
|
||||
|
||||
watchAdminToast(adminMessage, adminError)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -71,12 +83,14 @@ const {
|
||||
placeholder="Titel, Creator, Plattform, URL oder User suchen …"
|
||||
/>
|
||||
</label>
|
||||
<select v-model="platformFilter" class="h-12 rounded-2xl border border-violet-200 bg-white px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100">
|
||||
<option v-for="filter in platformFilters" :key="filter.key" :value="filter.key">{{ filter.label }} · {{ filter.count }}</option>
|
||||
</select>
|
||||
<select v-model="categoryFilter" class="h-12 rounded-2xl border border-violet-200 bg-white px-4 text-sm outline-none transition focus:border-violet-400 focus:ring-4 focus:ring-violet-100">
|
||||
<option v-for="filter in categoryFilters" :key="filter.id" :value="filter.id">{{ filter.label }} · {{ filter.count }}</option>
|
||||
</select>
|
||||
<NativeSelect
|
||||
v-model="platformFilter"
|
||||
:options="platformFilters.map((filter) => ({ label: `${filter.label} · ${filter.count}`, value: filter.key }))"
|
||||
/>
|
||||
<NativeSelect
|
||||
v-model="categoryFilter"
|
||||
:options="categoryFilters.map((filter) => ({ label: `${filter.label} · ${filter.count}`, value: filter.id }))"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
@@ -92,11 +106,35 @@ const {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="adminMessage" class="border-b border-emerald-100 bg-emerald-50 px-5 py-3 text-sm text-emerald-700">{{ adminMessage }}</p>
|
||||
<p v-if="adminError" class="border-b border-rose-100 bg-rose-50 px-5 py-3 text-sm text-rose-700">{{ adminError }}</p>
|
||||
<div v-if="clips.length > 0" class="flex flex-wrap items-center justify-between gap-3 border-b border-violet-100 bg-violet-50/40 px-5 py-3">
|
||||
<p class="text-sm text-slate-600">
|
||||
<strong class="text-violet-800">{{ clips.length }}</strong>
|
||||
{{ clips.length === 1 ? 'Clip' : 'Clips' }} in dieser Ansicht
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
class="gap-1.5 !bg-emerald-600 hover:!bg-emerald-500"
|
||||
size="sm"
|
||||
:disabled="bulkSaving || statusSaving !== null"
|
||||
@click="bulkUpdateStatus('approved')"
|
||||
>
|
||||
<CheckCircle2 class="h-3.5 w-3.5" />
|
||||
{{ bulkSaving ? 'Verarbeite …' : 'Alle freigeben' }}
|
||||
</Button>
|
||||
<Button
|
||||
class="gap-1.5 !bg-rose-600 hover:!bg-rose-500"
|
||||
size="sm"
|
||||
:disabled="bulkSaving || statusSaving !== null"
|
||||
@click="bulkUpdateStatus('rejected')"
|
||||
>
|
||||
<XCircle class="h-3.5 w-3.5" />
|
||||
Alle ablehnen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-violet-50">
|
||||
<div v-for="clip in clips" :key="clip.id" class="grid gap-4 px-5 py-5 xl:grid-cols-[minmax(0,1fr)_minmax(320px,420px)_320px] xl:items-start">
|
||||
<div v-for="clip in pagedClips" :key="clip.id" class="grid gap-4 px-5 py-5 xl:grid-cols-[minmax(0,1fr)_minmax(320px,420px)_320px] xl:items-start">
|
||||
<div class="flex min-w-0 gap-4">
|
||||
<div class="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-[linear-gradient(135deg,#ece2ff,#fff2dd)] text-violet-600">
|
||||
<Film class="h-5 w-5" />
|
||||
@@ -186,6 +224,15 @@ const {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PaginationFooter
|
||||
:page="page"
|
||||
:total-pages="totalPages"
|
||||
:range-start="rangeStart"
|
||||
:range-end="rangeEnd"
|
||||
:filtered-count="clips.length"
|
||||
@update:page="page = $event"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal :open="!!clipToDelete" title="Clip entfernen?" @close="clipToDelete = null">
|
||||
|
||||
Reference in New Issue
Block a user