admin_users_personels
This commit is contained in:
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user