admin_nyelvi_javítas

This commit is contained in:
Roo
2026-07-25 10:22:03 +00:00
parent 33c4d793db
commit c9118bf52f
29 changed files with 4474 additions and 943 deletions

View File

@@ -357,70 +357,139 @@
</div>
</div>
<!-- Commission Section (L2) -->
<!-- Commission Section (L2) - Százalékos Jutalékok -->
<div
class="bg-slate-700/30 rounded-lg p-4 space-y-4 transition-opacity duration-200"
:class="form.rule_type === 'L1_REWARD' ? 'opacity-40 pointer-events-none' : ''"
>
<div class="text-sm font-medium text-slate-300 mb-2">{{ t('commission_rules.commission_section') }}</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Commission Percent (Gen1) -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('commission_rules.commission_percent') }}</label>
<div class="relative">
<!-- Two-Column Layout: Gen1 (Left) / Gen2 (Right) -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Left Column: Gen1 - Közvetlen Ajánló -->
<div class="space-y-4">
<h4 class="text-sm font-semibold text-indigo-300 mb-4 border-b border-slate-600/50 pb-2">{{ t('commission_rules.gen1_title') }}</h4>
<!-- Commission Percent (Gen1) -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('commission_rules.commission_percent') }}</label>
<div class="relative">
<input
v-model.number="form.commission_percent"
type="number"
min="0"
max="100"
step="0.01"
placeholder="0.00"
:disabled="form.rule_type === 'L1_REWARD'"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm pr-8 disabled:cursor-not-allowed"
/>
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm">%</span>
</div>
<p class="mt-1 text-xs text-slate-400">{{ t('commission_rules.commission_percent_helper') }}</p>
</div>
<!-- Renewal Commission Percent -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('commission_rules.renewal_commission_percent') }}</label>
<div class="relative">
<input
v-model.number="form.renewal_commission_percent"
type="number"
min="0"
max="100"
step="0.01"
placeholder="0.00"
:disabled="form.rule_type === 'L1_REWARD'"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm pr-8 disabled:cursor-not-allowed"
/>
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm">%</span>
</div>
<p class="mt-1 text-xs text-slate-400">{{ t('commission_rules.renewal_commission_percent_helper') }}</p>
</div>
<!-- Gen1 Max Amount (Phase 2) -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('commission_rules.gen1_max_amount') }}</label>
<input
v-model.number="form.commission_percent"
v-model.number="form.gen1_max_amount"
type="number"
min="0"
max="100"
step="0.01"
placeholder="0.00"
:disabled="form.rule_type === 'L1_REWARD'"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm pr-8 disabled:cursor-not-allowed"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm disabled:cursor-not-allowed"
/>
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm">%</span>
<p class="mt-1 text-xs text-slate-400">{{ t('commission_rules.gen1_max_amount_helper') }}</p>
</div>
<p class="mt-1 text-xs text-slate-400">{{ t('commission_rules.commission_percent_helper') }}</p>
</div>
<!-- Upline Commission Percent (Gen2) -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('commission_rules.upline_commission_percent') }}</label>
<div class="relative">
<!-- Right Column: Gen2 - Felsővonal -->
<div class="space-y-4">
<h4 class="text-sm font-semibold text-emerald-300 mb-4 border-b border-slate-600/50 pb-2">{{ t('commission_rules.gen2_title') }}</h4>
<!-- Upline Commission Percent (Gen2) -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('commission_rules.upline_commission_percent') }}</label>
<div class="relative">
<input
v-model.number="form.upline_commission_percent"
type="number"
min="0"
max="100"
step="0.01"
placeholder="0.00"
:disabled="form.rule_type === 'L1_REWARD'"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm pr-8 disabled:cursor-not-allowed"
/>
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm">%</span>
</div>
<p class="mt-1 text-xs text-slate-400">{{ t('commission_rules.upline_commission_percent_helper') }}</p>
</div>
<!-- Gen2 Renewal Percent (Phase 2) -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('commission_rules.gen2_renewal_percent') }}</label>
<div class="relative">
<input
v-model.number="form.gen2_renewal_percent"
type="number"
min="0"
max="100"
step="0.01"
placeholder="0.00"
:disabled="form.rule_type === 'L1_REWARD'"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm pr-8 disabled:cursor-not-allowed"
/>
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm">%</span>
</div>
<p class="mt-1 text-xs text-slate-400">{{ t('commission_rules.gen2_renewal_percent_helper') }}</p>
</div>
<!-- Gen2 Max Amount (Phase 2) -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('commission_rules.gen2_max_amount') }}</label>
<input
v-model.number="form.upline_commission_percent"
v-model.number="form.gen2_max_amount"
type="number"
min="0"
max="100"
step="0.01"
placeholder="0.00"
:disabled="form.rule_type === 'L1_REWARD'"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm pr-8 disabled:cursor-not-allowed"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm disabled:cursor-not-allowed"
/>
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm">%</span>
<p class="mt-1 text-xs text-slate-400">{{ t('commission_rules.gen2_max_amount_helper') }}</p>
</div>
<p class="mt-1 text-xs text-slate-400">{{ t('commission_rules.upline_commission_percent_helper') }}</p>
</div>
<!-- Renewal Commission Percent -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('commission_rules.renewal_commission_percent') }}</label>
<div class="relative">
<input
v-model.number="form.renewal_commission_percent"
type="number"
min="0"
max="100"
step="0.01"
placeholder="0.00"
:disabled="form.rule_type === 'L1_REWARD'"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm pr-8 disabled:cursor-not-allowed"
/>
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-sm">%</span>
</div>
<p class="mt-1 text-xs text-slate-400">{{ t('commission_rules.renewal_commission_percent_helper') }}</p>
</div>
<!-- Commission Max Amount -->
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">{{ t('commission_rules.commission_max_amount') }}</label>
</div>
<!-- Legacy Field: Commission Max Amount (Fallback) - full width below columns -->
<div class="border-t border-slate-600/30 pt-4 mt-2">
<div class="max-w-xs">
<label class="block text-sm font-medium text-slate-400 mb-1">
{{ t('commission_rules.commission_max_amount') }}
<span class="text-xs text-slate-500 ml-1 italic">({{ t('commission_rules.legacy_fallback') }})</span>
</label>
<input
v-model.number="form.commission_max_amount"
type="number"
@@ -428,9 +497,9 @@
step="0.01"
placeholder="0.00"
:disabled="form.rule_type === 'L1_REWARD'"
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm disabled:cursor-not-allowed"
class="w-full px-3 py-2 bg-slate-700/50 border border-slate-600/50 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-sm disabled:cursor-not-allowed"
/>
<p class="mt-1 text-xs text-slate-400">{{ t('commission_rules.commission_max_amount_helper') }}</p>
<p class="mt-1 text-xs text-slate-500">{{ t('commission_rules.commission_max_amount_helper') }}</p>
</div>
</div>
</div>
@@ -481,6 +550,38 @@
</div>
</div>
<!-- Conflict Override Confirmation Modal -->
<div v-if="showConflictModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showConflictModal = false">
<div class="bg-slate-800 rounded-xl border border-amber-500/30 p-6 w-full max-w-lg mx-4 shadow-2xl">
<div class="flex items-start gap-3 mb-4">
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-amber-500/10 flex items-center justify-center">
<svg class="w-5 h-5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<div>
<h2 class="text-lg font-semibold text-white">{{ t('commission_rules.conflict_title') }}</h2>
<p class="text-sm text-slate-400 mt-1">{{ t('commission_rules.conflict_message') }}</p>
<div v-if="conflictingRule" class="mt-3 bg-slate-700/30 rounded-lg p-3 text-xs text-slate-300 space-y-1">
<div><span class="text-slate-500">ID:</span> {{ conflictingRule.id }}</div>
<div><span class="text-slate-500">{{ t('commission_rules.name') }}:</span> {{ conflictingRule.name }}</div>
<div><span class="text-slate-500">{{ t('commission_rules.tier') }}:</span> {{ conflictingRule.tier }}</div>
<div><span class="text-slate-500">{{ t('commission_rules.region') }}:</span> {{ conflictingRule.region_code }}</div>
</div>
</div>
</div>
<div class="flex justify-end gap-3 pt-2 border-t border-slate-700">
<button @click="showConflictModal = false; conflictingRule = null" class="px-4 py-2 text-sm text-slate-400 hover:text-white transition">{{ t('commission_rules.conflict_cancel') }}</button>
<button @click="confirmOverride" :disabled="saving" class="px-4 py-2 bg-amber-600 hover:bg-amber-500 disabled:bg-amber-600/50 text-white rounded-lg text-sm font-medium 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>
{{ saving ? t('commission_rules.saving') : t('commission_rules.conflict_confirm') }}
</button>
</div>
</div>
</div>
<!-- Success Toast -->
<div v-if="toast" class="fixed bottom-6 right-6 z-50 bg-emerald-600 text-white px-4 py-3 rounded-lg shadow-lg text-sm flex items-center gap-2 animate-slide-up">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -511,6 +612,9 @@ interface CommissionRule {
upline_commission_percent: number | null
renewal_commission_percent: number | null
commission_max_amount: number | null
gen1_max_amount: number | null
gen2_max_amount: number | null
gen2_renewal_percent: number | null
is_campaign: boolean
start_date: string | null
end_date: string | null
@@ -534,8 +638,11 @@ const error = ref(false)
const rules = ref<CommissionRule[]>([])
const showModal = ref(false)
const showDeactivateModal = ref(false)
const showConflictModal = ref(false)
const editingRule = ref<CommissionRule | null>(null)
const deactivatingRule = ref<CommissionRule | null>(null)
const conflictingRule = ref<CommissionRule | null>(null)
const pendingPayload = ref<Record<string, any> | null>(null)
const saving = ref(false)
const formError = ref('')
const toast = ref('')
@@ -578,6 +685,9 @@ const form = reactive({
upline_commission_percent: null as number | null,
renewal_commission_percent: null as number | null,
commission_max_amount: null as number | null,
gen1_max_amount: null as number | null,
gen2_max_amount: null as number | null,
gen2_renewal_percent: null as number | null,
is_active: true,
})
@@ -607,6 +717,9 @@ function resetForm() {
form.upline_commission_percent = null
form.renewal_commission_percent = null
form.commission_max_amount = null
form.gen1_max_amount = null
form.gen2_max_amount = null
form.gen2_renewal_percent = null
form.is_active = true
formError.value = ''
}
@@ -633,6 +746,9 @@ function openEditModal(rule: CommissionRule) {
form.upline_commission_percent = rule.upline_commission_percent
form.renewal_commission_percent = rule.renewal_commission_percent
form.commission_max_amount = rule.commission_max_amount
form.gen1_max_amount = rule.gen1_max_amount
form.gen2_max_amount = rule.gen2_max_amount
form.gen2_renewal_percent = rule.gen2_renewal_percent
form.is_active = rule.is_active
formError.value = ''
showModal.value = true
@@ -728,6 +844,9 @@ async function saveRule() {
payload.upline_commission_percent = form.upline_commission_percent
payload.renewal_commission_percent = form.renewal_commission_percent
payload.commission_max_amount = form.commission_max_amount
payload.gen1_max_amount = form.gen1_max_amount
payload.gen2_max_amount = form.gen2_max_amount
payload.gen2_renewal_percent = form.gen2_renewal_percent
if (editingRule.value) {
await $fetch(`/api/v1/admin/commission-rules/${editingRule.value.id}`, {
@@ -748,7 +867,78 @@ async function saveRule() {
await fetchRules()
} catch (e: any) {
console.error('Failed to save commission rule:', e)
formError.value = e?.data?.detail || t('commission_rules.save_error')
// Phase 3: Handle 409 Conflict — show override confirmation modal
if (e?.response?.status === 409 || e?.status === 409) {
const detail = e?.data?.detail || e?.response?._data?.detail || {}
const conflicting = detail?.conflicting_rule
if (conflicting) {
conflictingRule.value = conflicting as CommissionRule
pendingPayload.value = {
name: form.name,
description: form.description || null,
rule_type: form.rule_type,
tier: form.tier,
region_code: form.region_code,
is_campaign: form.is_campaign,
is_active: form.is_active,
start_date: form.is_campaign ? (form.start_date || null) : null,
end_date: form.is_campaign ? (form.end_date || null) : null,
xp_reward: form.xp_reward,
credit_reward: form.credit_reward,
commission_percent: form.commission_percent,
upline_commission_percent: form.upline_commission_percent,
renewal_commission_percent: form.renewal_commission_percent,
commission_max_amount: form.commission_max_amount,
gen1_max_amount: form.gen1_max_amount,
gen2_max_amount: form.gen2_max_amount,
gen2_renewal_percent: form.gen2_renewal_percent,
}
showConflictModal.value = true
return // Don't clear form error — modal handles it
}
}
formError.value = e?.data?.detail || e?.message || t('commission_rules.save_error')
} finally {
saving.value = false
}
}
async function confirmOverride() {
if (!pendingPayload.value) return
saving.value = true
formError.value = ''
try {
// Add force_override flag
const payload = { ...pendingPayload.value, force_override: true }
if (editingRule.value) {
await $fetch(`/api/v1/admin/commission-rules/${editingRule.value.id}`, {
method: 'PUT',
headers: getHeaders(),
body: payload,
})
showToast(t('commission_rules.updated'))
} else {
await $fetch('/api/v1/admin/commission-rules', {
method: 'POST',
headers: getHeaders(),
body: payload,
})
showToast(t('commission_rules.created'))
}
showConflictModal.value = false
conflictingRule.value = null
pendingPayload.value = null
closeModal()
await fetchRules()
} catch (e: any) {
console.error('Failed to save commission rule with override:', e)
formError.value = e?.data?.detail || e?.message || t('commission_rules.save_error')
showConflictModal.value = false
conflictingRule.value = null
pendingPayload.value = null
} finally {
saving.value = false
}