admin_users_personels

This commit is contained in:
Roo
2026-06-29 14:11:15 +00:00
parent 383e5511b6
commit df4a0f07ff
237 changed files with 17849 additions and 2934 deletions

View File

@@ -54,10 +54,46 @@
</button>
</div>
<!-- Package Tier Cards Grid -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 mb-8">
<!-- Package Content (shown when packages exist) -->
<div v-else class="space-y-6 mb-8">
<!-- Package Type Tabs (Base vs Add-on) -->
<div class="mb-6">
<div class="flex gap-2 border-b border-slate-700">
<button
@click="listTab = 'base'"
class="px-5 py-3 text-sm font-medium border-b-2 transition"
:class="listTab === 'base'
? 'border-indigo-500 text-indigo-300'
: 'border-transparent text-slate-400 hover:text-slate-200 hover:border-slate-500'"
>
<span 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="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
Alapcsomagok
</span>
</button>
<button
@click="listTab = 'addon'"
class="px-5 py-3 text-sm font-medium border-b-2 transition"
:class="listTab === 'addon'
? 'border-amber-500 text-amber-300'
: 'border-transparent text-slate-400 hover:text-slate-200 hover:border-slate-500'"
>
<span 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 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Kiegészítők
</span>
</button>
</div>
</div>
<!-- Package Tier Cards Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
<div
v-for="pkg in packages"
v-for="pkg in listTab === 'base' ? basePackages : addonPackages"
:key="pkg.id"
class="bg-slate-800 rounded-xl border border-slate-700 p-6 flex flex-col transition-all hover:border-slate-600"
>
@@ -122,6 +158,10 @@
<span class="text-slate-400">{{ $t('packages.max_garages') }}</span>
<span class="text-slate-200 font-medium">{{ getAllowance(pkg, 'max_garages') }}</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-slate-400">{{ $t('packages.max_users') }}</span>
<span class="text-slate-200 font-medium">{{ getAllowance(pkg, 'max_users') }}</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-slate-400">{{ $t('packages.monthly_credits') }}</span>
<span class="text-slate-200 font-medium">{{ getAllowance(pkg, 'monthly_free_credits') }}</span>
@@ -163,7 +203,8 @@
</button>
</div>
</div>
</div>
</div> <!-- /grid -->
</div> <!-- /package-content-wrapper -->
<!-- Create / Edit Modal (Tabbed Layout) -->
<div
@@ -187,6 +228,71 @@
</button>
</div>
<!-- 🎯 GLOBAL TYPE SELECTOR (above tabs, below title) -->
<div class="px-6 pt-4 pb-2 mb-6">
<div class="bg-slate-700/40 border border-slate-600 rounded-xl p-4">
<label class="block text-sm font-semibold text-slate-200 mb-3">{{ $t('packages.form_type') }}</label>
<div class="flex gap-4">
<label
class="flex-1 flex items-center gap-3 px-4 py-3 rounded-lg border-2 cursor-pointer transition"
:class="form.type === 'base' || form.type === 'private' || form.type === 'corporate'
? 'border-indigo-500 bg-indigo-500/10'
: 'border-slate-600 bg-slate-700/50 hover:border-slate-500'"
>
<input
type="radio"
name="packageType"
value="private"
v-model="form.type"
class="sr-only"
/>
<div
class="w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0"
:class="form.type === 'private' || form.type === 'corporate' || form.type === 'base'
? 'border-indigo-500'
: 'border-slate-500'"
>
<div
v-if="form.type === 'private' || form.type === 'corporate' || form.type === 'base'"
class="w-2.5 h-2.5 rounded-full bg-indigo-500"
></div>
</div>
<div>
<span class="text-sm font-medium text-white">📦 Alapcsomag (Base Tier)</span>
<p class="text-xs text-slate-400 mt-0.5">Teljes jogú csomag járművekkel, garázsokkal, felhasználókkal</p>
</div>
</label>
<label
class="flex-1 flex items-center gap-3 px-4 py-3 rounded-lg border-2 cursor-pointer transition"
:class="form.type === 'addon'
? 'border-amber-500 bg-amber-500/10'
: 'border-slate-600 bg-slate-700/50 hover:border-slate-500'"
>
<input
type="radio"
name="packageType"
value="addon"
v-model="form.type"
class="sr-only"
/>
<div
class="w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0"
:class="form.type === 'addon' ? 'border-amber-500' : 'border-slate-500'"
>
<div
v-if="form.type === 'addon'"
class="w-2.5 h-2.5 rounded-full bg-amber-500"
></div>
</div>
<div>
<span class="text-sm font-medium text-white"> Kiegészítő (Add-on)</span>
<p class="text-xs text-slate-400 mt-0.5">Kiegészítő modul meglévő csomaghoz, korlátozott opciókkal</p>
</div>
</label>
</div>
</div>
</div>
<!-- Tab Navigation -->
<div class="flex border-b border-slate-700 px-6">
<button
@@ -206,7 +312,7 @@
<div class="flex-1 overflow-y-auto px-6 py-5">
<!-- TAB 1: Basic Info -->
<div v-if="activeTab === 'basic'" class="space-y-4">
<!-- Name -->
<!-- System Name: free-text input for base, dropdown for addon -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ $t('packages.form_name') }}</label>
<input
@@ -228,33 +334,21 @@
/>
</div>
<!-- Type & Tier Level -->
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ $t('packages.form_type') }}</label>
<select
v-model="form.type"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
>
<option value="private">{{ $t('packages.type_private') }}</option>
<option value="corporate">{{ $t('packages.type_corporate') }}</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ $t('packages.form_tier_level') }}</label>
<input
v-model.number="form.tier_level"
type="number"
min="0"
max="10"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
<p class="text-xs text-slate-500 mt-1">{{ $t('packages.form_tier_level_hint') }}</p>
</div>
<!-- Tier Level (hidden for add-on) -->
<div v-if="form.type !== 'addon'">
<label class="block text-sm font-medium text-slate-300 mb-1">{{ $t('packages.form_tier_level') }}</label>
<input
v-model.number="form.tier_level"
type="number"
min="0"
max="10"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
<p class="text-xs text-slate-500 mt-1">{{ $t('packages.form_tier_level_hint') }}</p>
</div>
<!-- Subtitle & Badge -->
<div class="grid grid-cols-2 gap-4">
<!-- Subtitle & Badge (hidden for add-on) -->
<div v-if="form.type !== 'addon'" class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ $t('packages.form_subtitle') }}</label>
<input
@@ -286,8 +380,8 @@
<label for="is_custom" class="text-sm text-slate-300">{{ $t('packages.form_is_custom') }}</label>
</div>
<!-- Singleton Rules -->
<div class="border-t border-slate-700 pt-4">
<!-- Singleton Rules (hidden for add-on) -->
<div v-if="form.type !== 'addon'" class="border-t border-slate-700 pt-4">
<h4 class="text-sm font-semibold text-slate-300 mb-3">{{ $t('packages.singleton_rules') }}</h4>
<div class="space-y-4">
<div class="flex items-center gap-3">
@@ -381,15 +475,26 @@
/>
</td>
<td class="py-2 px-3 text-center">
<button
@click="removeZone(region.country_code)"
class="p-1.5 rounded-lg text-red-400 hover:text-red-300 hover:bg-red-500/10 transition"
:title="'Remove ' + region.country_code"
>
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
<div class="flex items-center gap-1">
<button
@click="applyDefaultPricingToZone(region.country_code)"
class="p-1.5 rounded-lg text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/10 transition"
:title="'Apply DEFAULT pricing to ' + region.country_code"
>
<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="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</button>
<button
@click="removeZone(region.country_code)"
class="p-1.5 rounded-lg text-red-400 hover:text-red-300 hover:bg-red-500/10 transition"
:title="'Remove ' + region.country_code"
>
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>
</tr>
</tbody>
@@ -521,6 +626,15 @@
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label class="block text-xs text-slate-400 mb-1">{{ $t('packages.max_users') }}</label>
<input
v-model.number="form.max_users"
type="number"
min="1"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
/>
</div>
<div>
<label class="block text-xs text-slate-400 mb-1">{{ $t('packages.monthly_credits') }}</label>
<input
@@ -699,6 +813,7 @@ interface SubscriptionTier {
allowances?: {
max_vehicles: number
max_garages: number
max_users: number
monthly_free_credits: number
} | null
marketing?: {
@@ -728,6 +843,7 @@ interface PackageForm {
currency: string
max_vehicles: number
max_garages: number
max_users: number
monthly_free_credits: number
max_cost_category_depth: number
advanced_reports: boolean
@@ -756,6 +872,13 @@ const activeTabLabel = computed(() => {
return tab ? tab.label : ''
})
// ── List Tab State (Base vs Add-on) ────────────────────────────────
const listTab = ref<'base' | 'addon'>('base')
const basePackages = computed(() => packages.value.filter(p => p.rules?.type !== 'addon'))
const addonPackages = computed(() => packages.value.filter(p => p.rules?.type === 'addon'))
// ── State ──────────────────────────────────────────────────────────
const packages = ref<SubscriptionTier[]>([])
@@ -822,6 +945,7 @@ const defaultForm: PackageForm = {
currency: 'EUR',
max_vehicles: 1,
max_garages: 1,
max_users: 1,
monthly_free_credits: 0,
max_cost_category_depth: 3,
advanced_reports: false,
@@ -902,6 +1026,32 @@ function applyCalculatorToActiveRegion() {
showNotification('success', `Applied net price ${formatCurrency(calcNetPrice, calcCurrency)} to ${code} region`)
}
/**
* Copy DEFAULT zone pricing values to the specified region.
* Uses Object.assign for Vue reactivity to ensure the UI updates immediately.
*/
function applyDefaultPricingToZone(countryCode: string) {
const defaultZone = form.pricing_zones['DEFAULT']
if (!defaultZone) {
showNotification('error', 'No DEFAULT pricing zone exists. Set DEFAULT prices first.')
return
}
if (countryCode === 'DEFAULT') return
// Ensure the target zone object exists
if (!form.pricing_zones[countryCode]) {
form.pricing_zones[countryCode] = { monthly_price: 0, yearly_price: 0, credit_price: 0, currency: 'EUR' }
}
// Copy DEFAULT values into the target zone using Object.assign for reactivity
const target = form.pricing_zones[countryCode]
Object.assign(target, {
monthly_price: defaultZone.monthly_price,
yearly_price: defaultZone.yearly_price,
credit_price: defaultZone.credit_price ?? 0,
currency: defaultZone.currency,
})
showNotification('success', `Applied DEFAULT pricing to ${countryCode} region`)
}
// ── API Helpers ────────────────────────────────────────────────────
function getAuthHeaders() {
@@ -914,7 +1064,7 @@ async function fetchPackages() {
loading.value = true
error.value = null
try {
const res = await $fetch<{ total: number; tiers: SubscriptionTier[] }>('/api/v1/admin/packages', {
const res = await $fetch<{ total: number; tiers: SubscriptionTier[] }>('/api/v1/admin/packages?include_hidden=true', {
headers: getAuthHeaders(),
})
packages.value = res.tiers || []
@@ -976,6 +1126,10 @@ function openCreateModal() {
editingPackage.value = null
activeTab.value = 'basic'
Object.assign(form, defaultForm)
// Context-aware type: if on the Add-on tab, default to addon type
if (listTab.value === 'addon') {
form.type = 'addon'
}
showModal.value = true
}
@@ -997,6 +1151,7 @@ function openEditModal(pkg: SubscriptionTier) {
form.currency = pricing.currency || 'EUR'
form.max_vehicles = allowances.max_vehicles || 1
form.max_garages = allowances.max_garages || 1
form.max_users = allowances.max_users || 1
form.monthly_free_credits = allowances.monthly_free_credits || 0
form.subtitle = rules.marketing?.subtitle || ''
form.badge = rules.marketing?.badge || ''
@@ -1056,6 +1211,7 @@ function duplicatePackage(pkg: SubscriptionTier) {
form.currency = pricing.currency || 'EUR'
form.max_vehicles = allowances.max_vehicles || 1
form.max_garages = allowances.max_garages || 1
form.max_users = allowances.max_users || 1
form.monthly_free_credits = allowances.monthly_free_credits || 0
form.subtitle = rules.marketing?.subtitle || ''
form.badge = rules.marketing?.badge || ''
@@ -1135,6 +1291,7 @@ async function savePackage() {
allowances: {
max_vehicles: form.max_vehicles,
max_garages: form.max_garages,
max_users: form.max_users,
monthly_free_credits: form.monthly_free_credits,
},
marketing: {