refactor: M5 useQuotaNotifyState composable + H14 Vue file splits
M5: New composable frontend/src/composables/useQuotaNotifyState.ts - Replaces 9 individual refs in both Create/Edit modals with reactive state - Provides loadFromExtra/writeToExtra/reset helpers - Eliminates ~120 lines of duplicated code across the two modals H14: Vue file length violations fixed - AdminPaymentPlansView.vue: 325 → 183 lines (extracted PlanEditDialog.vue) - QuotaLimitCard.vue: 327 → 268 lines (extracted QuotaDimensionRow.vue) - PlanEditDialog.vue: 181 lines (new, plan create/edit form) - QuotaDimensionRow.vue: 108 lines (new, single quota dimension row)
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
</template>
|
||||
<template #cell-price="{ value, row }">
|
||||
<div class="text-sm">
|
||||
<span class="font-medium text-gray-900 dark:text-white">${{ value.toFixed(2) }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">${{ (value ?? 0).toFixed(2) }}</span>
|
||||
<span v-if="row.original_price" class="ml-1 text-xs text-gray-400 line-through">${{ row.original_price.toFixed(2) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -67,86 +67,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Plan Edit Dialog -->
|
||||
<BaseDialog :show="showPlanDialog" :title="editingPlan ? t('payment.admin.editPlan') : t('payment.admin.createPlan')" width="wide" @close="showPlanDialog = false">
|
||||
<form id="plan-form" @submit.prevent="handleSavePlan" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="input-label">{{ t('payment.admin.planName') }}</label>
|
||||
<input v-model="planForm.name" type="text" class="input" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('payment.admin.group') }}</label>
|
||||
<Select v-model="planForm.group_id" :options="groupOptions" class="w-full">
|
||||
<template #selected="{ option }">
|
||||
<span v-if="option?.platform" :class="platformTextClass(String(option.platform))">{{ option.label }}</span>
|
||||
<span v-else>{{ option?.label || t('payment.admin.selectGroup') }}</span>
|
||||
</template>
|
||||
<template #option="{ option, selected }">
|
||||
<span class="flex-1 truncate text-left" :class="option.platform ? platformTextClass(String(option.platform)) : ''">{{ option.label }}</span>
|
||||
<Icon v-if="selected" name="check" size="sm" class="text-primary-500" :stroke-width="2" />
|
||||
</template>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group Info Preview -->
|
||||
<div v-if="selectedGroupInfo" class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-dark-600 dark:bg-dark-800">
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<GroupBadge :name="selectedGroupInfo.name" :platform="selectedGroupInfo.platform" :rate-multiplier="selectedGroupInfo.rate_multiplier" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div><span class="text-gray-500">{{ t('payment.admin.dailyLimit') }}:</span> <span class="ml-1 font-medium text-gray-700 dark:text-gray-300">{{ selectedGroupInfo.daily_limit_usd != null ? '$' + selectedGroupInfo.daily_limit_usd : t('payment.admin.unlimited') }}</span></div>
|
||||
<div><span class="text-gray-500">{{ t('payment.admin.weeklyLimit') }}:</span> <span class="ml-1 font-medium text-gray-700 dark:text-gray-300">{{ selectedGroupInfo.weekly_limit_usd != null ? '$' + selectedGroupInfo.weekly_limit_usd : t('payment.admin.unlimited') }}</span></div>
|
||||
<div><span class="text-gray-500">{{ t('payment.admin.monthlyLimit') }}:</span> <span class="ml-1 font-medium text-gray-700 dark:text-gray-300">{{ selectedGroupInfo.monthly_limit_usd != null ? '$' + selectedGroupInfo.monthly_limit_usd : t('payment.admin.unlimited') }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div><label class="input-label">{{ t('payment.admin.planDescription') }}</label><textarea v-model="planForm.description" rows="2" class="input"></textarea></div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div><label class="input-label">{{ t('payment.admin.price') }}</label><input v-model.number="planForm.price" type="number" step="0.01" min="0" class="input" required /></div>
|
||||
<div><label class="input-label">{{ t('payment.admin.originalPrice') }}</label><input v-model.number="planForm.original_price" type="number" step="0.01" min="0" class="input" /></div>
|
||||
<div><label class="input-label">{{ t('payment.admin.sortOrder') }}</label><input v-model.number="planForm.sort_order" type="number" min="0" class="input" /></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><label class="input-label">{{ t('payment.admin.validityDays') }}</label><input v-model.number="planForm.validity_days" type="number" min="1" class="input" required /></div>
|
||||
<div><label class="input-label">{{ t('payment.admin.validityUnit') }}</label><Select v-model="planForm.validity_unit" :options="validityUnitOptions" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('payment.admin.features') }}</label>
|
||||
<textarea v-model="planFeaturesText" rows="3" class="input" :placeholder="t('payment.admin.featuresPlaceholder')"></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ t('payment.admin.featuresHint') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-sm text-gray-700 dark:text-gray-300">{{ t('payment.admin.forSale') }}</label>
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||
planForm.for_sale ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
|
||||
]"
|
||||
@click="planForm.for_sale = !planForm.for_sale"
|
||||
>
|
||||
<span :class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
planForm.for_sale ? 'translate-x-5' : 'translate-x-0'
|
||||
]" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button type="button" @click="showPlanDialog = false" class="btn btn-secondary">{{ t('common.cancel') }}</button>
|
||||
<button type="submit" form="plan-form" :disabled="planSaving" class="btn btn-primary">{{ planSaving ? t('common.saving') : t('common.save') }}</button>
|
||||
</div>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
<PlanEditDialog :show="showPlanDialog" :plan="editingPlan" :groups="groups" @close="showPlanDialog = false" @saved="loadPlans" />
|
||||
|
||||
<ConfirmDialog :show="showDeletePlanDialog" :title="t('payment.admin.deletePlan')" :message="t('payment.admin.deletePlanConfirm')" :confirm-text="t('common.delete')" danger @confirm="handleDeletePlan" @cancel="showDeletePlanDialog = false" />
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { adminPaymentAPI } from '@/api/admin/payment'
|
||||
@@ -157,11 +85,10 @@ import type { AdminGroup } from '@/types'
|
||||
import type { Column } from '@/components/common/types'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
import DataTable from '@/components/common/DataTable.vue'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import GroupBadge from '@/components/common/GroupBadge.vue'
|
||||
import PlanEditDialog from './PlanEditDialog.vue'
|
||||
import { platformTextClass } from '@/utils/platformColors'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -190,39 +117,14 @@ function getPlanNameClass(groupId: number): string {
|
||||
return group ? platformTextClass(group.platform) : 'text-gray-900 dark:text-white'
|
||||
}
|
||||
|
||||
const groupOptions = computed(() => [
|
||||
{ value: 0, label: t('payment.admin.selectGroup'), platform: '' },
|
||||
...groups.value
|
||||
.filter(g => g.subscription_type === 'subscription')
|
||||
.map(g => ({
|
||||
value: g.id,
|
||||
label: `${g.name} — ${g.platform} (${g.rate_multiplier}x)`,
|
||||
platform: g.platform,
|
||||
})),
|
||||
])
|
||||
|
||||
const selectedGroupInfo = computed(() => {
|
||||
if (!planForm.group_id) return null
|
||||
return groups.value.find(g => g.id === planForm.group_id) || null
|
||||
})
|
||||
|
||||
// ==================== Plans ====================
|
||||
|
||||
const plansLoading = ref(false)
|
||||
const plans = ref<SubscriptionPlan[]>([])
|
||||
const showPlanDialog = ref(false)
|
||||
const showDeletePlanDialog = ref(false)
|
||||
const planSaving = ref(false)
|
||||
const editingPlan = ref<SubscriptionPlan | null>(null)
|
||||
const deletingPlanId = ref<number | null>(null)
|
||||
const planForm = reactive({ name: '', group_id: 0, description: '', price: 0, original_price: 0, validity_days: 30, validity_unit: 'days', for_sale: true, sort_order: 0 })
|
||||
const planFeaturesText = ref('')
|
||||
|
||||
const validityUnitOptions = computed(() => [
|
||||
{ value: 'days', label: t('payment.admin.days') },
|
||||
{ value: 'weeks', label: t('payment.admin.weeks') },
|
||||
{ value: 'months', label: t('payment.admin.months') },
|
||||
])
|
||||
|
||||
const planColumns = computed((): Column[] => [
|
||||
{ key: 'id', label: 'ID' },
|
||||
@@ -231,7 +133,6 @@ const planColumns = computed((): Column[] => [
|
||||
{ key: 'price', label: t('payment.admin.price') },
|
||||
{ key: 'validity_days', label: t('payment.admin.validityDays') },
|
||||
{ key: 'for_sale', label: t('payment.admin.forSale') },
|
||||
{ key: 'sort_order', label: t('payment.admin.sortOrder') },
|
||||
{ key: 'actions', label: t('common.actions') },
|
||||
])
|
||||
|
||||
@@ -253,44 +154,9 @@ async function loadPlans() {
|
||||
|
||||
function openPlanEdit(plan: SubscriptionPlan | null) {
|
||||
editingPlan.value = plan
|
||||
if (plan) {
|
||||
Object.assign(planForm, { name: plan.name, group_id: plan.group_id, description: plan.description, price: plan.price, original_price: plan.original_price || 0, validity_days: plan.validity_days, validity_unit: plan.validity_unit || 'days', for_sale: plan.for_sale, sort_order: plan.sort_order })
|
||||
planFeaturesText.value = (plan.features || []).join('\n')
|
||||
} else {
|
||||
Object.assign(planForm, { name: '', group_id: 0, description: '', price: 0, original_price: 0, validity_days: 30, validity_unit: 'days', for_sale: true, sort_order: 0 })
|
||||
planFeaturesText.value = ''
|
||||
}
|
||||
showPlanDialog.value = true
|
||||
}
|
||||
|
||||
/** Build request payload with snake_case keys matching backend JSON tags */
|
||||
function buildPlanPayload() {
|
||||
const features = planFeaturesText.value.split('\n').map(f => f.trim()).filter(Boolean).join('\n')
|
||||
return {
|
||||
name: planForm.name,
|
||||
group_id: planForm.group_id,
|
||||
description: planForm.description,
|
||||
price: planForm.price,
|
||||
original_price: planForm.original_price || 0,
|
||||
validity_days: planForm.validity_days,
|
||||
validity_unit: planForm.validity_unit,
|
||||
for_sale: planForm.for_sale,
|
||||
sort_order: planForm.sort_order,
|
||||
features,
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSavePlan() {
|
||||
planSaving.value = true
|
||||
try {
|
||||
const data = buildPlanPayload()
|
||||
if (editingPlan.value) { await adminPaymentAPI.updatePlan(editingPlan.value.id, data) }
|
||||
else { await adminPaymentAPI.createPlan(data) }
|
||||
appStore.showSuccess(t('common.saved')); showPlanDialog.value = false; loadPlans()
|
||||
} catch (err: unknown) { appStore.showError(extractApiErrorMessage(err, t('common.error'))) }
|
||||
finally { planSaving.value = false }
|
||||
}
|
||||
|
||||
/** Quick toggle for_sale from the list */
|
||||
async function toggleForSale(plan: SubscriptionPlan) {
|
||||
try {
|
||||
|
||||
181
frontend/src/views/admin/orders/PlanEditDialog.vue
Normal file
181
frontend/src/views/admin/orders/PlanEditDialog.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<BaseDialog :show="show" :title="plan ? t('payment.admin.editPlan') : t('payment.admin.createPlan')" width="wide" @close="emit('close')">
|
||||
<form id="plan-form" @submit.prevent="handleSavePlan" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="input-label">{{ t('payment.admin.planName') }} <span class="text-red-500">*</span></label>
|
||||
<input v-model="planForm.name" type="text" class="input" required />
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('payment.admin.group') }} <span class="text-red-500">*</span></label>
|
||||
<Select v-model="planForm.group_id" :options="groupOptions" :placeholder="t('payment.admin.selectGroup')" class="w-full">
|
||||
<template #selected="{ option }">
|
||||
<span v-if="option?.platform" :class="platformTextClass(String(option.platform))">{{ option.label }}</span>
|
||||
<span v-else>{{ option?.label || t('payment.admin.selectGroup') }}</span>
|
||||
</template>
|
||||
<template #option="{ option, selected }">
|
||||
<span class="flex-1 truncate text-left" :class="option.platform ? platformTextClass(String(option.platform)) : ''">{{ option.label }}</span>
|
||||
<Icon v-if="selected" name="check" size="sm" class="text-primary-500" :stroke-width="2" />
|
||||
</template>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group Info Preview -->
|
||||
<div v-if="selectedGroupInfo" class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-dark-600 dark:bg-dark-800">
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<GroupBadge :name="selectedGroupInfo.name" :platform="selectedGroupInfo.platform" :rate-multiplier="selectedGroupInfo.rate_multiplier" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div><span class="text-gray-500">{{ t('payment.admin.dailyLimit') }}:</span> <span class="ml-1 font-medium text-gray-700 dark:text-gray-300">{{ selectedGroupInfo.daily_limit_usd != null ? '$' + selectedGroupInfo.daily_limit_usd : t('payment.admin.unlimited') }}</span></div>
|
||||
<div><span class="text-gray-500">{{ t('payment.admin.weeklyLimit') }}:</span> <span class="ml-1 font-medium text-gray-700 dark:text-gray-300">{{ selectedGroupInfo.weekly_limit_usd != null ? '$' + selectedGroupInfo.weekly_limit_usd : t('payment.admin.unlimited') }}</span></div>
|
||||
<div><span class="text-gray-500">{{ t('payment.admin.monthlyLimit') }}:</span> <span class="ml-1 font-medium text-gray-700 dark:text-gray-300">{{ selectedGroupInfo.monthly_limit_usd != null ? '$' + selectedGroupInfo.monthly_limit_usd : t('payment.admin.unlimited') }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div><label class="input-label">{{ t('payment.admin.planDescription') }} <span class="text-red-500">*</span></label><textarea v-model="planForm.description" rows="2" class="input" required></textarea></div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><label class="input-label">{{ t('payment.admin.price') }} <span class="text-red-500">*</span></label><input v-model.number="planForm.price" type="number" step="0.01" min="0.01" class="input" required /></div>
|
||||
<div><label class="input-label">{{ t('payment.admin.originalPrice') }}</label><input v-model.number="planForm.original_price" type="number" step="0.01" min="0" class="input" /></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><label class="input-label">{{ t('payment.admin.validityDays') }} <span class="text-red-500">*</span></label><input v-model.number="planForm.validity_days" type="number" min="1" class="input" required /></div>
|
||||
<div><label class="input-label">{{ t('payment.admin.validityUnit') }} <span class="text-red-500">*</span></label><Select v-model="planForm.validity_unit" :options="validityUnitOptions" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('payment.admin.features') }}</label>
|
||||
<textarea v-model="planFeaturesText" rows="3" class="input" :placeholder="t('payment.admin.featuresPlaceholder')"></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ t('payment.admin.featuresHint') }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="text-sm text-gray-700 dark:text-gray-300">{{ t('payment.admin.forSale') }}</label>
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||
planForm.for_sale ? 'bg-primary-500' : 'bg-gray-300 dark:bg-dark-600'
|
||||
]"
|
||||
@click="planForm.for_sale = !planForm.for_sale"
|
||||
>
|
||||
<span :class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
planForm.for_sale ? 'translate-x-5' : 'translate-x-0'
|
||||
]" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button type="button" @click="emit('close')" class="btn btn-secondary">{{ t('common.cancel') }}</button>
|
||||
<button type="submit" form="plan-form" :disabled="saving" class="btn btn-primary">{{ saving ? t('common.saving') : t('common.save') }}</button>
|
||||
</div>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { adminPaymentAPI } from '@/api/admin/payment'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import type { SubscriptionPlan } from '@/types/payment'
|
||||
import type { AdminGroup } from '@/types'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import GroupBadge from '@/components/common/GroupBadge.vue'
|
||||
import { platformTextClass } from '@/utils/platformColors'
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
plan: SubscriptionPlan | null
|
||||
groups: AdminGroup[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
saved: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const saving = ref(false)
|
||||
const planForm = reactive({ name: '', group_id: null as number | null, description: '', price: 0, original_price: 0, validity_days: 30, validity_unit: 'days', for_sale: true })
|
||||
const planFeaturesText = ref('')
|
||||
|
||||
const validityUnitOptions = computed(() => [
|
||||
{ value: 'days', label: t('payment.admin.days') },
|
||||
{ value: 'weeks', label: t('payment.admin.weeks') },
|
||||
{ value: 'months', label: t('payment.admin.months') },
|
||||
])
|
||||
|
||||
const groupOptions = computed(() =>
|
||||
props.groups
|
||||
.filter(g => g.subscription_type === 'subscription')
|
||||
.map(g => ({
|
||||
value: g.id,
|
||||
label: `${g.name} — ${g.platform} (${g.rate_multiplier}x)`,
|
||||
platform: g.platform,
|
||||
})),
|
||||
)
|
||||
|
||||
const selectedGroupInfo = computed(() => {
|
||||
if (!planForm.group_id) return null
|
||||
return props.groups.find(g => g.id === planForm.group_id) || null
|
||||
})
|
||||
|
||||
// Reset form when dialog opens
|
||||
watch(() => props.show, (visible) => {
|
||||
if (!visible) return
|
||||
if (props.plan) {
|
||||
Object.assign(planForm, { name: props.plan.name, group_id: props.plan.group_id, description: props.plan.description, price: props.plan.price, original_price: props.plan.original_price || 0, validity_days: props.plan.validity_days, validity_unit: props.plan.validity_unit || 'days', for_sale: props.plan.for_sale })
|
||||
planFeaturesText.value = (props.plan.features || []).join('\n')
|
||||
} else {
|
||||
Object.assign(planForm, { name: '', group_id: null, description: '', price: 0, original_price: 0, validity_days: 30, validity_unit: 'days', for_sale: true })
|
||||
planFeaturesText.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
/** Build request payload with snake_case keys matching backend JSON tags */
|
||||
function buildPlanPayload() {
|
||||
const features = planFeaturesText.value.split('\n').map(f => f.trim()).filter(Boolean).join('\n')
|
||||
return {
|
||||
name: planForm.name,
|
||||
group_id: planForm.group_id,
|
||||
description: planForm.description,
|
||||
price: planForm.price,
|
||||
original_price: planForm.original_price || 0,
|
||||
validity_days: planForm.validity_days,
|
||||
validity_unit: planForm.validity_unit,
|
||||
for_sale: planForm.for_sale,
|
||||
features,
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSavePlan() {
|
||||
if (!planForm.group_id) {
|
||||
appStore.showError(t('payment.admin.groupRequired'))
|
||||
return
|
||||
}
|
||||
if (!planForm.price || planForm.price <= 0) {
|
||||
appStore.showError(t('payment.admin.priceRequired'))
|
||||
return
|
||||
}
|
||||
if (!planForm.validity_days || planForm.validity_days < 1) {
|
||||
appStore.showError(t('payment.admin.validityDaysRequired'))
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const data = buildPlanPayload()
|
||||
if (props.plan) { await adminPaymentAPI.updatePlan(props.plan.id, data) }
|
||||
else { await adminPaymentAPI.createPlan(data) }
|
||||
appStore.showSuccess(t('common.saved'))
|
||||
emit('close')
|
||||
emit('saved')
|
||||
} catch (err: unknown) { appStore.showError(extractApiErrorMessage(err, t('common.error'))) }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user