2026-08-08 11:20:26 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
|
import { message, Modal } from 'ant-design-vue'
|
|
|
|
|
|
import { ArrowRightOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
|
|
|
|
|
import api from '@/api/http'
|
|
|
|
|
|
import JalaliDatePicker from '@/components/JalaliDatePicker.vue'
|
|
|
|
|
|
import MoneyInput from '@/components/MoneyInput.vue'
|
2026-08-08 12:35:59 +03:30
|
|
|
|
import TenantPanelShortcuts from '@/components/TenantPanelShortcuts.vue'
|
2026-08-08 11:20:26 +03:30
|
|
|
|
import { formatIranDate, todayIranDateInput, iranDateInputToIso } from '@/utils/datetime'
|
|
|
|
|
|
import { formatMoney } from '@/utils/money'
|
|
|
|
|
|
|
|
|
|
|
|
type ExpiryLevel = 'healthy' | 'warning' | 'critical' | 'unknown' | 'none'
|
|
|
|
|
|
|
|
|
|
|
|
type SupportItem = {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
name: string
|
|
|
|
|
|
specs: string | null
|
|
|
|
|
|
description: string | null
|
|
|
|
|
|
priceRials: number | null
|
|
|
|
|
|
isFree: boolean
|
|
|
|
|
|
chargedAt: string | null
|
|
|
|
|
|
planMonths: number | null
|
|
|
|
|
|
expiresAt: string | null
|
|
|
|
|
|
alertDaysBefore: number
|
|
|
|
|
|
isActive: boolean
|
|
|
|
|
|
summary: {
|
|
|
|
|
|
expiryLevel: ExpiryLevel
|
|
|
|
|
|
expiryLabel: string
|
|
|
|
|
|
planLabel: string | null
|
|
|
|
|
|
expiresAt: string | null
|
|
|
|
|
|
}
|
|
|
|
|
|
charges: any[]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const saving = ref(false)
|
|
|
|
|
|
const renewing = ref(false)
|
|
|
|
|
|
const creating = ref(false)
|
|
|
|
|
|
const selectedId = ref<string | null>(null)
|
|
|
|
|
|
const showCreate = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
const tenant = ref<{
|
|
|
|
|
|
id: string
|
|
|
|
|
|
product: 'hamkar' | 'khanegi'
|
|
|
|
|
|
slug: string
|
|
|
|
|
|
nameFa: string
|
|
|
|
|
|
domain: string
|
|
|
|
|
|
} | null>(null)
|
|
|
|
|
|
const supports = ref<SupportItem[]>([])
|
|
|
|
|
|
const planOptions = ref<{ months: number; label: string }[]>([])
|
|
|
|
|
|
|
|
|
|
|
|
const form = reactive({
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
specs: '',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
priceRials: 0 as number,
|
|
|
|
|
|
isFree: false,
|
|
|
|
|
|
chargedAt: null as string | null,
|
|
|
|
|
|
planMonths: 1 as number,
|
|
|
|
|
|
alertDaysBefore: 3,
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const createForm = reactive({
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
specs: '',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
priceRials: 0 as number,
|
|
|
|
|
|
isFree: false,
|
|
|
|
|
|
chargedAt: iranDateInputToIso(todayIranDateInput()) as string | null,
|
|
|
|
|
|
planMonths: 1,
|
|
|
|
|
|
alertDaysBefore: 3,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const renewForm = reactive({
|
|
|
|
|
|
chargedAt: iranDateInputToIso(todayIranDateInput()) as string | null,
|
|
|
|
|
|
planMonths: 1,
|
|
|
|
|
|
priceRials: 0 as number,
|
|
|
|
|
|
isFree: false,
|
|
|
|
|
|
note: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const tenantId = computed(() => String(route.params.tenantId || ''))
|
|
|
|
|
|
|
|
|
|
|
|
const selected = computed(() => supports.value.find((p) => p.id === selectedId.value) || null)
|
|
|
|
|
|
|
|
|
|
|
|
const planSelectOptions = computed(() =>
|
|
|
|
|
|
(planOptions.value.length
|
|
|
|
|
|
? planOptions.value
|
|
|
|
|
|
: [
|
|
|
|
|
|
{ months: 1, label: 'یکماهه' },
|
|
|
|
|
|
{ months: 3, label: 'سهماهه' },
|
|
|
|
|
|
{ months: 6, label: 'ششماهه' },
|
|
|
|
|
|
{ months: 12, label: 'یکساله' },
|
|
|
|
|
|
]
|
|
|
|
|
|
).map((p) => ({ value: p.months, label: p.label })),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
function toneClass(level?: ExpiryLevel) {
|
|
|
|
|
|
return level && level !== 'none' ? level : 'unknown'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function fillForm(item: SupportItem) {
|
|
|
|
|
|
Object.assign(form, {
|
|
|
|
|
|
name: item.name || '',
|
|
|
|
|
|
specs: item.specs || '',
|
|
|
|
|
|
description: item.description || '',
|
|
|
|
|
|
priceRials: item.isFree ? 0 : (item.priceRials ?? 0),
|
|
|
|
|
|
isFree: Boolean(item.isFree),
|
|
|
|
|
|
chargedAt: item.chargedAt || null,
|
|
|
|
|
|
planMonths: item.planMonths || 1,
|
|
|
|
|
|
alertDaysBefore: item.alertDaysBefore ?? 3,
|
|
|
|
|
|
isActive: item.isActive !== false,
|
|
|
|
|
|
})
|
|
|
|
|
|
renewForm.planMonths = item.planMonths || 1
|
|
|
|
|
|
renewForm.isFree = Boolean(item.isFree)
|
|
|
|
|
|
renewForm.priceRials = item.isFree ? 0 : (item.priceRials ?? 0)
|
|
|
|
|
|
renewForm.chargedAt = iranDateInputToIso(todayIranDateInput())
|
|
|
|
|
|
renewForm.note = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function selectSupport(id: string) {
|
|
|
|
|
|
selectedId.value = id
|
|
|
|
|
|
const item = supports.value.find((p) => p.id === id)
|
|
|
|
|
|
if (item) fillForm(item)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadDetail() {
|
|
|
|
|
|
if (!tenantId.value) return
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.get(`/support/tenants/${tenantId.value}`)
|
|
|
|
|
|
tenant.value = data.tenant
|
|
|
|
|
|
supports.value = data.supports || []
|
|
|
|
|
|
planOptions.value = data.planOptions || []
|
|
|
|
|
|
|
|
|
|
|
|
const keep =
|
|
|
|
|
|
selectedId.value && supports.value.some((p) => p.id === selectedId.value)
|
|
|
|
|
|
? selectedId.value
|
|
|
|
|
|
: supports.value[0]?.id || null
|
|
|
|
|
|
selectedId.value = keep
|
|
|
|
|
|
if (keep) {
|
|
|
|
|
|
const item = supports.value.find((p) => p.id === keep)
|
|
|
|
|
|
if (item) fillForm(item)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'خطا در دریافت پشتیبانیها')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function createSupport() {
|
|
|
|
|
|
if (!createForm.name.trim()) {
|
|
|
|
|
|
message.warning('نام پشتیبانی لازم است')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
creating.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const isFree = Boolean(createForm.isFree)
|
|
|
|
|
|
const { data } = await api.post(`/support/tenants/${tenantId.value}`, {
|
|
|
|
|
|
name: createForm.name.trim(),
|
|
|
|
|
|
specs: createForm.specs || undefined,
|
|
|
|
|
|
description: createForm.description || undefined,
|
|
|
|
|
|
isFree,
|
|
|
|
|
|
priceRials: isFree ? 0 : Number(createForm.priceRials) || 0,
|
|
|
|
|
|
chargedAt: createForm.chargedAt || undefined,
|
|
|
|
|
|
planMonths: Number(createForm.planMonths),
|
|
|
|
|
|
alertDaysBefore: Number(createForm.alertDaysBefore) || 3,
|
|
|
|
|
|
})
|
|
|
|
|
|
message.success('پشتیبانی اضافه شد')
|
|
|
|
|
|
showCreate.value = false
|
|
|
|
|
|
createForm.name = ''
|
|
|
|
|
|
createForm.specs = ''
|
|
|
|
|
|
createForm.description = ''
|
|
|
|
|
|
createForm.isFree = false
|
|
|
|
|
|
createForm.priceRials = 0
|
|
|
|
|
|
createForm.chargedAt = iranDateInputToIso(todayIranDateInput())
|
|
|
|
|
|
createForm.planMonths = 1
|
|
|
|
|
|
createForm.alertDaysBefore = 3
|
|
|
|
|
|
await loadDetail()
|
|
|
|
|
|
if (data?.support?.id) selectSupport(data.support.id)
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
const msg = e?.response?.data?.message
|
|
|
|
|
|
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'ثبت ناموفق')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
creating.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveSupport() {
|
|
|
|
|
|
if (!selectedId.value) return
|
|
|
|
|
|
if (!form.name.trim()) {
|
|
|
|
|
|
message.warning('نام پشتیبانی لازم است')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
saving.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const isFree = Boolean(form.isFree)
|
|
|
|
|
|
await api.patch(`/support/items/${selectedId.value}`, {
|
|
|
|
|
|
name: form.name.trim(),
|
|
|
|
|
|
specs: form.specs || null,
|
|
|
|
|
|
description: form.description || null,
|
|
|
|
|
|
isFree,
|
|
|
|
|
|
priceRials: isFree ? 0 : Number(form.priceRials) || 0,
|
|
|
|
|
|
chargedAt: form.chargedAt,
|
|
|
|
|
|
planMonths: Number(form.planMonths),
|
|
|
|
|
|
alertDaysBefore: Number(form.alertDaysBefore) || 3,
|
|
|
|
|
|
isActive: form.isActive,
|
|
|
|
|
|
})
|
|
|
|
|
|
message.success('ذخیره شد')
|
|
|
|
|
|
await loadDetail()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
const msg = e?.response?.data?.message
|
|
|
|
|
|
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'ذخیره ناموفق')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function renewSupport() {
|
|
|
|
|
|
if (!selectedId.value) return
|
|
|
|
|
|
if (!renewForm.chargedAt) {
|
|
|
|
|
|
message.warning('تاریخ شارژ لازم است')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
renewing.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const isFree = Boolean(renewForm.isFree)
|
|
|
|
|
|
await api.post(`/support/items/${selectedId.value}/renew`, {
|
|
|
|
|
|
chargedAt: renewForm.chargedAt,
|
|
|
|
|
|
planMonths: Number(renewForm.planMonths),
|
|
|
|
|
|
isFree,
|
|
|
|
|
|
priceRials: isFree ? 0 : Number(renewForm.priceRials),
|
|
|
|
|
|
note: renewForm.note || undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
message.success('پشتیبانی شارژ/تمدید شد')
|
|
|
|
|
|
renewForm.note = ''
|
|
|
|
|
|
await loadDetail()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
const msg = e?.response?.data?.message
|
|
|
|
|
|
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'شارژ ناموفق')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
renewing.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function confirmDelete() {
|
|
|
|
|
|
if (!selected.value) return
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: 'حذف پشتیبانی',
|
|
|
|
|
|
content: `پشتیبانی «${selected.value.name}» و تاریخچه شارژ آن حذف شود؟`,
|
|
|
|
|
|
okText: 'حذف',
|
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
|
cancelText: 'انصراف',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.delete(`/support/items/${selected.value!.id}`)
|
|
|
|
|
|
message.success('حذف شد')
|
|
|
|
|
|
selectedId.value = null
|
|
|
|
|
|
await loadDetail()
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'حذف ناموفق')
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => form.isFree,
|
|
|
|
|
|
(free) => {
|
|
|
|
|
|
if (free) form.priceRials = 0
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => createForm.isFree,
|
|
|
|
|
|
(free) => {
|
|
|
|
|
|
if (free) createForm.priceRials = 0
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => renewForm.isFree,
|
|
|
|
|
|
(free) => {
|
|
|
|
|
|
if (free) renewForm.priceRials = 0
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(() => route.params.tenantId, loadDetail)
|
|
|
|
|
|
onMounted(loadDetail)
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="page-shell">
|
|
|
|
|
|
<div style="display: flex; align-items: flex-start; gap: 8px; margin-bottom: 16px">
|
|
|
|
|
|
<a-button type="text" @click="router.push({ name: 'support-panel' })">
|
|
|
|
|
|
<ArrowRightOutlined />
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<div style="flex: 1">
|
|
|
|
|
|
<h1 class="page-title">{{ tenant?.nameFa || 'پشتیبانیهای نسخه' }}</h1>
|
|
|
|
|
|
<p class="page-sub">
|
|
|
|
|
|
{{ tenant?.product === 'hamkar' ? 'همکار' : 'خانگی' }} · {{ tenant?.domain }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-button type="primary" @click="showCreate = true">
|
|
|
|
|
|
<PlusOutlined /> پشتیبانی جدید
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-08 12:35:59 +03:30
|
|
|
|
<TenantPanelShortcuts :tenant-id="tenantId" current="support" />
|
|
|
|
|
|
|
2026-08-08 11:20:26 +03:30
|
|
|
|
<a-spin :spinning="loading">
|
|
|
|
|
|
<div class="layout">
|
|
|
|
|
|
<aside class="support-list">
|
|
|
|
|
|
<div class="list-head">پشتیبانیها ({{ supports.length }})</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="p in supports"
|
|
|
|
|
|
:key="p.id"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="support-card"
|
|
|
|
|
|
:class="[toneClass(p.summary?.expiryLevel), { active: p.id === selectedId, inactive: !p.isActive }]"
|
|
|
|
|
|
@click="selectSupport(p.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<strong>
|
|
|
|
|
|
{{ p.name }}
|
|
|
|
|
|
<a-tag v-if="p.isFree" color="blue" class="free-tag">رایگان</a-tag>
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
<span>{{ p.summary?.expiryLabel }}</span>
|
|
|
|
|
|
<small v-if="!p.isActive">غیرفعال</small>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div v-if="!supports.length" class="empty-hint">
|
|
|
|
|
|
هنوز پشتیبانیای ثبت نشده — با «پشتیبانی جدید» شروع کنید.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="selected" class="detail">
|
|
|
|
|
|
<div class="status-banner" :class="toneClass(selected.summary?.expiryLevel)">
|
|
|
|
|
|
<strong>
|
|
|
|
|
|
{{ selected.name }} — {{ selected.summary?.expiryLabel }}
|
|
|
|
|
|
<a-tag v-if="selected.isFree" color="blue" class="free-tag">رایگان</a-tag>
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
<span v-if="selected.summary?.expiresAt">
|
|
|
|
|
|
انقضا: {{ formatIranDate(selected.summary.expiresAt) }}
|
|
|
|
|
|
<template v-if="selected.summary?.planLabel"> · {{ selected.summary.planLabel }}</template>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="form-section">
|
|
|
|
|
|
<div class="section-head">
|
|
|
|
|
|
<h3>مشخصات پشتیبانی</h3>
|
|
|
|
|
|
<a-button danger type="text" @click="confirmDelete">
|
|
|
|
|
|
<DeleteOutlined /> حذف
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
|
<a-form-item label="نام پشتیبانی" required>
|
|
|
|
|
|
<a-input v-model:value="form.name" size="large" placeholder="مثلاً پشتیبانی پایه / ویژه" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="مشخصات">
|
|
|
|
|
|
<a-textarea v-model:value="form.specs" :rows="2" placeholder="جزئیات فنی یا یادداشت داخلی…" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="توضیحات">
|
|
|
|
|
|
<a-textarea v-model:value="form.description" :rows="2" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="فعال">
|
|
|
|
|
|
<a-switch v-model:checked="form.isActive" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="form-section">
|
|
|
|
|
|
<h3>اعتبار و قیمت (وضعیت فعلی)</h3>
|
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
|
<a-row :gutter="12">
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="تاریخ شارژ (شمسی)">
|
|
|
|
|
|
<JalaliDatePicker v-model="form.chargedAt" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="مدت شارژ">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="form.planMonths"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:options="planSelectOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="دوره رایگان">
|
|
|
|
|
|
<a-switch v-model:checked="form.isFree" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
2026-08-08 13:27:31 +03:30
|
|
|
|
<a-form-item v-if="!form.isFree" label="قیمت دوره (تومان)">
|
|
|
|
|
|
<MoneyInput v-model:value="form.priceRials" placeholder="مثلاً 150,000" />
|
2026-08-08 11:20:26 +03:30
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<p v-else class="field-hint">
|
|
|
|
|
|
دوره رایگان فعال است — مبلغ صفر میشود. مدت ۱ / ۳ / ۶ / ۱۲ ماه میتواند رایگان باشد.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<a-form-item label="شروع هشدار SMS (روز مانده به انقضا)">
|
|
|
|
|
|
<a-input-number
|
|
|
|
|
|
v-model:value="form.alertDaysBefore"
|
|
|
|
|
|
:min="1"
|
|
|
|
|
|
:max="30"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
style="width: 160px"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span class="inline-hint">پیشفرض ۳ روز — روزانه یک پیام تا تمدید</span>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="sticky-actions settings-save">
|
|
|
|
|
|
<a-button type="primary" size="large" block :loading="saving" @click="saveSupport">
|
|
|
|
|
|
ذخیره تنظیمات
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="form-section renew-section">
|
|
|
|
|
|
<h3>شارژ / تمدید جدید</h3>
|
|
|
|
|
|
<p class="field-hint">
|
|
|
|
|
|
با ثبت شارژ، تاریخ انقضا تمدید میشود و هشدارهای قبلی قطع میگردد تا دوباره نزدیک پایان برسد.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
|
<a-row :gutter="12">
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="تاریخ شروع دوره (شمسی)">
|
|
|
|
|
|
<JalaliDatePicker v-model="renewForm.chargedAt" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="مدت">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="renewForm.planMonths"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:options="planSelectOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :xs="24" :md="8">
|
|
|
|
|
|
<a-form-item label="دوره رایگان">
|
|
|
|
|
|
<a-switch v-model:checked="renewForm.isFree" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
2026-08-08 13:27:31 +03:30
|
|
|
|
<a-form-item v-if="!renewForm.isFree" label="مبلغ (تومان)">
|
|
|
|
|
|
<MoneyInput v-model:value="renewForm.priceRials" placeholder="مثلاً 150,000" />
|
2026-08-08 11:20:26 +03:30
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<p v-else class="field-hint">
|
|
|
|
|
|
دوره رایگان — مبلغ صفر. مدت ۱ / ۳ / ۶ / ۱۲ ماه میتواند رایگان باشد.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<a-form-item label="یادداشت">
|
|
|
|
|
|
<a-input v-model:value="renewForm.note" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-button type="primary" size="large" :loading="renewing" @click="renewSupport">
|
|
|
|
|
|
ثبت شارژ / تمدید
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="form-section">
|
|
|
|
|
|
<h3>تاریخچه شارژ</h3>
|
|
|
|
|
|
<div v-if="selected.charges?.length" class="charge-list">
|
|
|
|
|
|
<article v-for="item in selected.charges" :key="item.id" class="charge-item">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong v-if="item.isFree">رایگان</strong>
|
|
|
|
|
|
<strong v-else>{{ formatMoney(item.priceRials) }}</strong>
|
|
|
|
|
|
<a-tag v-if="item.isFree" color="blue" class="free-tag">رایگان</a-tag>
|
|
|
|
|
|
<span> · {{ item.planMonths }} ماهه</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="charge-meta">
|
|
|
|
|
|
از {{ formatIranDate(item.chargedAt) }} تا {{ formatIranDate(item.expiresAt) }}
|
|
|
|
|
|
<template v-if="item.note"> · {{ item.note }}</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="hint-box">هنوز شارژی ثبت نشده است.</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="detail empty-detail">
|
|
|
|
|
|
پشتیبانیای انتخاب نشده است.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
v-model:open="showCreate"
|
|
|
|
|
|
title="افزودن پشتیبانی"
|
|
|
|
|
|
ok-text="ثبت"
|
|
|
|
|
|
cancel-text="انصراف"
|
|
|
|
|
|
:confirm-loading="creating"
|
|
|
|
|
|
@ok="createSupport"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-form layout="vertical" style="margin-top: 12px">
|
|
|
|
|
|
<a-form-item label="نام پشتیبانی" required>
|
|
|
|
|
|
<a-input v-model:value="createForm.name" size="large" placeholder="نام پشتیبانی" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="مشخصات">
|
|
|
|
|
|
<a-textarea v-model:value="createForm.specs" :rows="2" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-row :gutter="12">
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="تاریخ شارژ">
|
|
|
|
|
|
<JalaliDatePicker v-model="createForm.chargedAt" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
<a-col :span="12">
|
|
|
|
|
|
<a-form-item label="مدت">
|
|
|
|
|
|
<a-select
|
|
|
|
|
|
v-model:value="createForm.planMonths"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:options="planSelectOptions"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-col>
|
|
|
|
|
|
</a-row>
|
|
|
|
|
|
<a-form-item label="دوره رایگان">
|
|
|
|
|
|
<a-switch v-model:checked="createForm.isFree" />
|
|
|
|
|
|
</a-form-item>
|
2026-08-08 13:27:31 +03:30
|
|
|
|
<a-form-item v-if="!createForm.isFree" label="قیمت دوره (تومان)">
|
|
|
|
|
|
<MoneyInput v-model:value="createForm.priceRials" placeholder="مثلاً 150,000" />
|
2026-08-08 11:20:26 +03:30
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<p v-else class="field-hint">
|
|
|
|
|
|
دوره رایگان — مبلغ صفر. مدت ۱ / ۳ / ۶ / ۱۲ ماه میتواند رایگان باشد.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.layout {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(200px, 260px) 1fr;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
align-items: start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
|
.layout {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.support-list {
|
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #e8eef5;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.list-head {
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
color: #334155;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.support-card {
|
|
|
|
|
|
appearance: none;
|
|
|
|
|
|
border: 2px solid transparent;
|
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
color: #17324d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.support-card.active {
|
|
|
|
|
|
border-color: #3b6fd4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.support-card.inactive {
|
|
|
|
|
|
opacity: 0.65;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.support-card.healthy { background: linear-gradient(135deg, #dff8ea, #c3efd7); }
|
|
|
|
|
|
.support-card.warning { background: linear-gradient(135deg, #fff4c7, #ffe39a); }
|
|
|
|
|
|
.support-card.critical { background: linear-gradient(135deg, #ffd8d8, #ffb9b9); }
|
|
|
|
|
|
.support-card.unknown { background: linear-gradient(135deg, #eef2f7, #dde5ef); }
|
|
|
|
|
|
|
|
|
|
|
|
.free-tag {
|
|
|
|
|
|
margin-inline-start: 6px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
line-height: 18px;
|
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-hint,
|
|
|
|
|
|
.empty-detail,
|
|
|
|
|
|
.hint-box {
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
padding: 14px;
|
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-banner {
|
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
|
padding: 14px 16px;
|
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-banner.healthy { background: #dff8ea; color: #0f7a45; }
|
|
|
|
|
|
.status-banner.warning { background: #fff4c7; color: #9a6b00; }
|
|
|
|
|
|
.status-banner.critical { background: #ffd8d8; color: #b42318; }
|
|
|
|
|
|
.status-banner.unknown { background: #eef2f7; color: #64748b; }
|
|
|
|
|
|
|
|
|
|
|
|
.section-head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-hint {
|
|
|
|
|
|
margin: 0 0 8px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.settings-save {
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.renew-section {
|
|
|
|
|
|
margin-top: 28px;
|
|
|
|
|
|
padding-top: 22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.inline-hint {
|
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
|
color: #94a3b8;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.charge-list {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.charge-item {
|
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.charge-meta {
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|