Files
service-finder/frontend_admin/pages/gamification/seasons.vue
2026-06-30 08:56:55 +00:00

408 lines
15 KiB
Vue

<template>
<div>
<!-- Page Header -->
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-white">{{ t('gamification.seasons.title') }}</h1>
<p class="text-slate-400 mt-1">{{ t('gamification.seasons.subtitle') }}</p>
</div>
<button
class="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition"
@click="openCreateModal"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
{{ t('gamification.seasons.create_btn') }}
</button>
</div>
<!-- Loading State -->
<div v-if="loading" class="flex items-center justify-center py-20">
<div class="text-slate-400 flex items-center gap-3">
<svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>{{ t('gamification.seasons.loading') }}</span>
</div>
</div>
<!-- Error State -->
<div v-else-if="error" class="bg-rose-500/10 border border-rose-500/30 rounded-xl p-6 mb-8">
<p class="text-rose-400">{{ t('gamification.seasons.load_error') }}</p>
<button class="mt-2 text-sm text-rose-300 hover:text-rose-200 underline" @click="fetchSeasons">{{ t('gamification.seasons.retry') }}</button>
</div>
<!-- Seasons List -->
<template v-else>
<!-- Empty State -->
<div v-if="seasons.length === 0" class="bg-slate-800 rounded-xl border border-slate-700 p-12 text-center">
<svg class="w-12 h-12 mx-auto text-slate-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p class="text-slate-400">{{ t('gamification.seasons.empty') }}</p>
<button
class="mt-4 px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-sm font-medium rounded-lg transition"
@click="openCreateModal"
>
{{ t('gamification.seasons.create_first') }}
</button>
</div>
<!-- Season Cards -->
<div v-else class="space-y-4">
<div
v-for="season in seasons"
:key="season.id"
class="bg-slate-800 rounded-xl border border-slate-700 p-6 hover:border-slate-600 transition"
>
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="text-lg font-semibold text-white">{{ season.name }}</h3>
<span
class="px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="season.is_active
? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/30'
: 'bg-slate-700 text-slate-400 border border-slate-600'"
>
{{ season.is_active ? t('gamification.seasons.active') : t('gamification.seasons.inactive') }}
</span>
</div>
<div class="flex items-center gap-6 text-sm text-slate-400">
<div class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>{{ formatDate(season.start_date) }} {{ formatDate(season.end_date) }}</span>
</div>
<div class="flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{{ t('gamification.seasons.created_at') }} {{ formatDateTime(season.created_at) }}</span>
</div>
</div>
</div>
<!-- Actions -->
<div class="flex items-center gap-2 ml-4">
<button
v-if="!season.is_active"
class="p-2 rounded-lg text-emerald-400 hover:bg-emerald-500/10 transition"
:title="t('gamification.seasons.activate')"
@click="activateSeason(season)"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
<button
class="p-2 rounded-lg text-indigo-400 hover:bg-indigo-500/10 transition"
:title="t('gamification.seasons.edit')"
@click="openEditModal(season)"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
</div>
</div>
</div>
</div>
</template>
<!-- Create/Edit Modal -->
<div
v-if="showModal"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
>
<div class="fixed inset-0 bg-black/60" @click="closeModal" />
<div class="relative bg-slate-800 rounded-xl border border-slate-700 w-full max-w-lg p-6 shadow-2xl">
<h2 class="text-lg font-semibold text-white mb-6">
{{ editingSeason ? t('gamification.seasons.edit_title') : t('gamification.seasons.create_title') }}
</h2>
<form @submit.prevent="saveSeason" class="space-y-4">
<!-- Name -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('gamification.seasons.name_label') }}</label>
<input
v-model="form.name"
type="text"
required
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
:placeholder="t('gamification.seasons.name_placeholder')"
/>
</div>
<!-- Start Date -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('gamification.seasons.start_date_label') }}</label>
<input
v-model="form.start_date"
type="date"
required
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<!-- End Date -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('gamification.seasons.end_date_label') }}</label>
<input
v-model="form.end_date"
type="date"
required
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<!-- Active toggle (only for create) -->
<div v-if="!editingSeason" class="flex items-center gap-3">
<input
v-model="form.is_active"
type="checkbox"
id="is_active"
class="w-4 h-4 rounded border-slate-600 bg-slate-700 text-indigo-600 focus:ring-indigo-500"
/>
<label for="is_active" class="text-sm text-slate-300">{{ t('gamification.seasons.active_on_create') }}</label>
</div>
<!-- Error -->
<div v-if="formError" class="bg-rose-500/10 border border-rose-500/30 rounded-lg p-3">
<p class="text-sm text-rose-400">{{ formError }}</p>
</div>
<!-- Buttons -->
<div class="flex items-center justify-end gap-3 pt-2">
<button
type="button"
class="px-4 py-2 text-sm text-slate-400 hover:text-white transition"
@click="closeModal"
>
{{ t('gamification.seasons.cancel') }}
</button>
<button
type="submit"
:disabled="saving"
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:bg-indigo-600/50 text-white text-sm font-medium rounded-lg transition flex items-center gap-2"
>
<svg v-if="saving" class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
{{ editingSeason ? t('gamification.seasons.save') : t('gamification.seasons.create') }}
</button>
</div>
</form>
</div>
</div>
<!-- Activate Confirmation Modal -->
<div
v-if="showActivateModal"
class="fixed inset-0 z-50 flex items-center justify-center p-4"
>
<div class="fixed inset-0 bg-black/60" @click="showActivateModal = false" />
<div class="relative bg-slate-800 rounded-xl border border-slate-700 w-full max-w-md p-6 shadow-2xl">
<h2 class="text-lg font-semibold text-white mb-2">{{ t('gamification.seasons.activate_title') }}</h2>
<p class="text-slate-400 text-sm mb-6">
{{ t('gamification.seasons.activate_confirm', { name: activatingSeason?.name }) }}
</p>
<div class="flex items-center justify-end gap-3">
<button
class="px-4 py-2 text-sm text-slate-400 hover:text-white transition"
@click="showActivateModal = false"
>
{{ t('gamification.seasons.cancel') }}
</button>
<button
:disabled="saving"
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-500 disabled:bg-emerald-600/50 text-white text-sm font-medium rounded-lg transition"
@click="confirmActivate"
>
{{ saving ? t('gamification.seasons.activating') : t('gamification.seasons.activate') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: 'auth',
})
const { t } = useI18n()
interface Season {
id: number
name: string
start_date: string
end_date: string
is_active: boolean
created_at: string | null
}
interface SeasonForm {
name: string
start_date: string
end_date: string
is_active: boolean
}
const loading = ref(true)
const error = ref(false)
const saving = ref(false)
const showModal = ref(false)
const showActivateModal = ref(false)
const seasons = ref<Season[]>([])
const editingSeason = ref<Season | null>(null)
const activatingSeason = ref<Season | null>(null)
const formError = ref('')
const form = reactive<SeasonForm>({
name: '',
start_date: '',
end_date: '',
is_active: false,
})
function getAuthHeaders() {
const tokenCookie = useCookie('access_token')
const token = tokenCookie.value
return token ? { Authorization: `Bearer ${token}` } : {}
}
function formatDate(dateStr: string): string {
if (!dateStr) return ''
const d = new Date(dateStr)
return d.toLocaleDateString('hu-HU', { year: 'numeric', month: 'long', day: 'numeric' })
}
function formatDateTime(dateStr: string | null): string {
if (!dateStr) return ''
const d = new Date(dateStr)
return d.toLocaleDateString('hu-HU', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' })
}
function resetForm() {
form.name = ''
form.start_date = ''
form.end_date = ''
form.is_active = false
formError.value = ''
}
function openCreateModal() {
editingSeason.value = null
resetForm()
showModal.value = true
}
function openEditModal(season: Season) {
editingSeason.value = season
form.name = season.name
form.start_date = season.start_date
form.end_date = season.end_date
form.is_active = season.is_active
formError.value = ''
showModal.value = true
}
function closeModal() {
showModal.value = false
editingSeason.value = null
resetForm()
}
function activateSeason(season: Season) {
activatingSeason.value = season
showActivateModal.value = true
}
async function fetchSeasons() {
loading.value = true
error.value = false
try {
const data = await $fetch('/api/v1/admin/gamification/seasons', {
headers: getAuthHeaders(),
})
seasons.value = data as Season[]
} catch (e) {
console.error('Failed to fetch seasons:', e)
error.value = true
} finally {
loading.value = false
}
}
async function saveSeason() {
saving.value = true
formError.value = ''
try {
if (editingSeason.value) {
// Update existing season
const payload: Record<string, any> = {}
if (form.name !== editingSeason.value.name) payload.name = form.name
if (form.start_date !== editingSeason.value.start_date) payload.start_date = form.start_date
if (form.end_date !== editingSeason.value.end_date) payload.end_date = form.end_date
if (Object.keys(payload).length > 0) {
await $fetch(`/api/v1/admin/gamification/seasons/${editingSeason.value.id}`, {
method: 'PUT',
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
body: payload,
})
}
} else {
// Create new season
await $fetch('/api/v1/admin/gamification/seasons', {
method: 'POST',
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
body: {
name: form.name,
start_date: form.start_date,
end_date: form.end_date,
is_active: form.is_active,
},
})
}
closeModal()
await fetchSeasons()
} catch (e: any) {
console.error('Failed to save season:', e)
formError.value = e?.data?.detail || e?.message || t('gamification.seasons.save_error')
} finally {
saving.value = false
}
}
async function confirmActivate() {
if (!activatingSeason.value) return
saving.value = true
try {
await $fetch(`/api/v1/admin/gamification/seasons/${activatingSeason.value.id}/activate`, {
method: 'POST',
headers: getAuthHeaders(),
})
showActivateModal.value = false
activatingSeason.value = null
await fetchSeasons()
} catch (e: any) {
console.error('Failed to activate season:', e)
} finally {
saving.value = false
}
}
onMounted(() => {
fetchSeasons()
})
</script>