admin frontend elkezdése, járművek tisztázása, frontend fejleszts

This commit is contained in:
Roo
2026-06-15 18:52:38 +00:00
parent ef8df9608c
commit 213ba3b0f1
80 changed files with 11615 additions and 2304 deletions

View 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>