admin frontend elkezdése, járművek tisztázása, frontend fejleszts
This commit is contained in:
349
frontend/src/views/admin/AdminServicesView.vue
Normal file
349
frontend/src/views/admin/AdminServicesView.vue
Normal file
@@ -0,0 +1,349 @@
|
||||
<template>
|
||||
<div class="admin-services-view">
|
||||
<!-- Page Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-white">{{ $t('admin.services.title') }}</h2>
|
||||
<p class="text-gray-400 mt-1">{{ $t('admin.services.subtitle') }}</p>
|
||||
</div>
|
||||
<button
|
||||
class="flex items-center gap-2 px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||
@click="openCreateModal"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{{ $t('admin.services.create_button') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<button
|
||||
class="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-gray-300 text-sm rounded-lg transition-colors"
|
||||
@click="loadServices"
|
||||
>
|
||||
{{ $t('admin.services.refresh') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="store.isLoading" class="text-center py-12 text-gray-400">
|
||||
{{ $t('admin.services.loading') }}
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="store.error" class="text-center py-12 text-red-400">
|
||||
{{ store.error }}
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div
|
||||
v-else-if="store.services.length === 0"
|
||||
class="text-center py-12 text-gray-500"
|
||||
>
|
||||
<span class="text-5xl block mb-4">🛠️</span>
|
||||
<p class="text-lg">{{ $t('admin.services.empty') }}</p>
|
||||
<p class="text-sm mt-1">{{ $t('admin.services.empty_hint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Service Cards Grid -->
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for="svc in store.services"
|
||||
:key="svc.id"
|
||||
class="relative bg-gray-800/60 rounded-xl border border-gray-700 p-6 hover:border-blue-500/30 transition-all group"
|
||||
:class="{ 'opacity-70': !svc.is_active }"
|
||||
>
|
||||
<!-- Inactive Badge -->
|
||||
<div
|
||||
v-if="!svc.is_active"
|
||||
class="absolute top-3 right-3 px-2 py-0.5 bg-yellow-900/60 text-yellow-300 border border-yellow-700 rounded-full text-xs font-medium"
|
||||
>
|
||||
{{ $t('admin.services.badge_inactive') }}
|
||||
</div>
|
||||
|
||||
<!-- Service Name & Description -->
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-bold text-white">{{ svc.name }}</h3>
|
||||
<!-- Service Code as small secondary badge -->
|
||||
<span class="inline-block mt-1 px-2 py-0.5 bg-gray-700/50 text-gray-500 border border-gray-600/50 rounded text-xs font-mono">
|
||||
{{ svc.service_code }}
|
||||
</span>
|
||||
<p v-if="svc.description" class="text-sm text-gray-400 mt-2 line-clamp-2">
|
||||
{{ svc.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Credit Cost -->
|
||||
<div class="border-t border-gray-700/50 pt-3 mb-4">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-400">{{ $t('admin.services.credit_cost_label') }}</span>
|
||||
<span class="text-gray-200 font-medium">{{ svc.credit_cost }} {{ $t('admin.services.credit_unit') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-2 pt-3 border-t border-gray-700/50">
|
||||
<button
|
||||
class="flex-1 px-3 py-2 text-sm font-medium text-blue-300 bg-blue-900/30 hover:bg-blue-900/50 border border-blue-700/50 rounded-lg transition-colors"
|
||||
@click="openEditModal(svc)"
|
||||
>
|
||||
{{ $t('admin.services.edit_button') }}
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 px-3 py-2 text-sm font-medium rounded-lg transition-colors"
|
||||
:class="svc.is_active
|
||||
? 'text-red-300 bg-red-900/30 hover:bg-red-900/50 border border-red-700/50'
|
||||
: 'text-green-300 bg-green-900/30 hover:bg-green-900/50 border border-green-700/50'"
|
||||
@click="toggleActive(svc)"
|
||||
>
|
||||
{{ svc.is_active ? $t('admin.services.deactivate_button') : $t('admin.services.activate_button') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit / Create Modal -->
|
||||
<div
|
||||
v-if="showModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||
@click.self="closeModal"
|
||||
>
|
||||
<div class="bg-gray-800 rounded-xl border border-gray-700 w-full max-w-lg mx-4 shadow-2xl">
|
||||
<!-- Modal Header -->
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-700">
|
||||
<h3 class="text-lg font-bold text-white">
|
||||
{{ editingService ? $t('admin.services.modal_edit_title') : $t('admin.services.modal_create_title') }}
|
||||
</h3>
|
||||
<button
|
||||
class="text-gray-400 hover:text-white transition-colors"
|
||||
@click="closeModal"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="px-6 py-4 space-y-4">
|
||||
<!-- Service Code (only for create) -->
|
||||
<div v-if="!editingService">
|
||||
<label class="block text-sm font-medium text-gray-300 mb-1">
|
||||
{{ $t('admin.services.field_code') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.service_code"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
|
||||
:placeholder="$t('admin.services.field_code_placeholder')"
|
||||
/>
|
||||
<p v-if="formErrors.service_code" class="text-red-400 text-xs mt-1">{{ formErrors.service_code }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-1">
|
||||
{{ $t('admin.services.field_name') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="form.name"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none"
|
||||
:placeholder="$t('admin.services.field_name_placeholder')"
|
||||
/>
|
||||
<p v-if="formErrors.name" class="text-red-400 text-xs mt-1">{{ formErrors.name }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-1">
|
||||
{{ $t('admin.services.field_description') }}
|
||||
</label>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
rows="3"
|
||||
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none resize-none"
|
||||
:placeholder="$t('admin.services.field_description_placeholder')"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Credit Cost (no spinners) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-300 mb-1">
|
||||
{{ $t('admin.services.field_credit_cost') }}
|
||||
</label>
|
||||
<input
|
||||
v-model.number="form.credit_cost"
|
||||
type="number"
|
||||
min="0"
|
||||
class="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none
|
||||
[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-700">
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium text-gray-300 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors"
|
||||
@click="closeModal"
|
||||
>
|
||||
{{ $t('admin.services.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="store.isLoading"
|
||||
@click="saveService"
|
||||
>
|
||||
<span v-if="store.isLoading">{{ $t('admin.services.saving') }}</span>
|
||||
<span v-else>{{ editingService ? $t('admin.services.save_changes') : $t('admin.services.create') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useAdminServicesStore } from '../../stores/adminServices'
|
||||
import type { ServiceCatalogItem } from '../../stores/adminServices'
|
||||
|
||||
const store = useAdminServicesStore()
|
||||
|
||||
// ── State ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const showModal = ref(false)
|
||||
const editingService = ref<ServiceCatalogItem | null>(null)
|
||||
|
||||
const form = reactive({
|
||||
service_code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
credit_cost: 0,
|
||||
})
|
||||
|
||||
const formErrors = reactive({
|
||||
service_code: '',
|
||||
name: '',
|
||||
})
|
||||
|
||||
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
||||
|
||||
onMounted(() => {
|
||||
loadServices()
|
||||
})
|
||||
|
||||
// ── Methods ─────────────────────────────────────────────────────────────────
|
||||
|
||||
async function loadServices() {
|
||||
await store.fetchServices()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.service_code = ''
|
||||
form.name = ''
|
||||
form.description = ''
|
||||
form.credit_cost = 0
|
||||
formErrors.service_code = ''
|
||||
formErrors.name = ''
|
||||
}
|
||||
|
||||
function validateForm(): boolean {
|
||||
let valid = true
|
||||
formErrors.service_code = ''
|
||||
formErrors.name = ''
|
||||
|
||||
if (!editingService) {
|
||||
if (!form.service_code || form.service_code.trim().length < 3) {
|
||||
formErrors.service_code = 'Service code must be at least 3 characters'
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
|
||||
if (!form.name || form.name.trim().length < 2) {
|
||||
formErrors.name = 'Name must be at least 2 characters'
|
||||
valid = false
|
||||
}
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
// ── Modal Handlers ──────────────────────────────────────────────────────────
|
||||
|
||||
function openCreateModal() {
|
||||
editingService.value = null
|
||||
resetForm()
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function openEditModal(svc: ServiceCatalogItem) {
|
||||
editingService.value = svc
|
||||
form.service_code = svc.service_code
|
||||
form.name = svc.name
|
||||
form.description = svc.description || ''
|
||||
form.credit_cost = svc.credit_cost
|
||||
formErrors.service_code = ''
|
||||
formErrors.name = ''
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false
|
||||
editingService.value = null
|
||||
resetForm()
|
||||
}
|
||||
|
||||
async function saveService() {
|
||||
if (!validateForm()) return
|
||||
|
||||
try {
|
||||
if (editingService.value) {
|
||||
await store.updateService(editingService.value.id, {
|
||||
name: form.name.trim(),
|
||||
description: form.description.trim() || null,
|
||||
credit_cost: form.credit_cost,
|
||||
})
|
||||
} else {
|
||||
await store.createService({
|
||||
service_code: form.service_code.trim().toUpperCase(),
|
||||
name: form.name.trim(),
|
||||
description: form.description.trim() || null,
|
||||
credit_cost: form.credit_cost,
|
||||
is_active: true,
|
||||
})
|
||||
}
|
||||
closeModal()
|
||||
await loadServices()
|
||||
} catch (err: any) {
|
||||
// Error is already set in the store
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleActive(svc: ServiceCatalogItem) {
|
||||
try {
|
||||
await store.updateService(svc.id, { is_active: !svc.is_active })
|
||||
await loadServices()
|
||||
} catch (err: any) {
|
||||
// Error is already set in the store
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-services-view {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user