feat: complete application updates
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,7 @@ const password = ref(isMockAuthEnabled() ? MOCK_ADMIN.password : '')
|
||||
const remember = ref(true)
|
||||
const showPassword = ref(false)
|
||||
const mockAuth = isMockAuthEnabled()
|
||||
const brandLogoUrl = `${import.meta.env.BASE_URL}brand-logo.png`
|
||||
|
||||
const isFormValid = computed(() => phone.value.trim().length > 0 && password.value.length > 0)
|
||||
|
||||
@@ -123,6 +124,7 @@ async function submit() {
|
||||
|
||||
<div class="login-page__brand">
|
||||
<div class="login-page__brand-inner">
|
||||
<img :src="brandLogoUrl" alt="نادمی تکس" class="login-page__brand-logo" />
|
||||
<p class="login-page__brand-badge">پارچه</p>
|
||||
<h1 class="login-page__brand-title">پنل مدیریت سفارشهای عمده</h1>
|
||||
<p class="login-page__brand-text">
|
||||
@@ -146,7 +148,12 @@ async function submit() {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 40px;
|
||||
background: linear-gradient(160deg, #062428 0%, #0a353c 55%, #0e454e 100%);
|
||||
background: linear-gradient(
|
||||
160deg,
|
||||
var(--color-primary-dark) 0%,
|
||||
var(--color-primary) 55%,
|
||||
var(--color-sidebar-active) 100%
|
||||
);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -154,6 +161,14 @@ async function submit() {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.login-page__brand-logo {
|
||||
display: block;
|
||||
width: min(220px, 70%);
|
||||
height: 150px;
|
||||
object-fit: contain;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.login-page__brand-badge {
|
||||
display: inline-flex;
|
||||
margin: 0 0 16px;
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import AdminCurrencyInput from '@/components/ui/AdminCurrencyInput.vue'
|
||||
import AdminPageSkeleton from '@/components/ui/AdminPageSkeleton.vue'
|
||||
import BaseSwitch from '@/components/ui/BaseSwitch.vue'
|
||||
import { useSmsWalletSummary } from '@/composables/useSmsWalletSummary'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { smsWalletApi, type SmsUsageStats, type SmsWalletManage } from '@/services/api'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { formatCardNumber, formatSheba } from '@/utils/smsFormat'
|
||||
|
||||
const toast = useToast()
|
||||
const { refresh: refreshSummary } = useSmsWalletSummary()
|
||||
|
||||
const loading = ref(true)
|
||||
const savingPrice = ref(false)
|
||||
const charging = ref(false)
|
||||
const savingDeposit = ref(false)
|
||||
const savingThresholds = ref(false)
|
||||
const toggling = ref(false)
|
||||
|
||||
const wallet = ref<SmsWalletManage | null>(null)
|
||||
const pricePerPart = ref<number | null>(250)
|
||||
const chargeAmount = ref<number | null>(null)
|
||||
const chargeNote = ref('')
|
||||
const cardNumber = ref('')
|
||||
const sheba = ref('')
|
||||
const accountName = ref('')
|
||||
const yellowThreshold = ref<number | null>(10000)
|
||||
const redThreshold = ref<number | null>(1000)
|
||||
|
||||
function apply(data: SmsWalletManage) {
|
||||
wallet.value = data
|
||||
pricePerPart.value = data.pricePerPart
|
||||
cardNumber.value = formatCardNumber(data.cardNumber)
|
||||
sheba.value = formatSheba(data.sheba)
|
||||
accountName.value = data.accountName
|
||||
yellowThreshold.value = data.yellowThreshold
|
||||
redThreshold.value = data.redThreshold
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
apply(await smsWalletApi.manage())
|
||||
await refreshSummary().catch(() => {})
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : 'دریافت تعرفه پیامک ناموفق بود.')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
async function savePrice() {
|
||||
if (!pricePerPart.value || pricePerPart.value < 1 || savingPrice.value) return
|
||||
savingPrice.value = true
|
||||
try {
|
||||
apply(await smsWalletApi.update({ pricePerPart: pricePerPart.value }))
|
||||
toast.success('تعرفه پیامک ذخیره شد.')
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : 'ذخیره تعرفه ناموفق بود.')
|
||||
} finally {
|
||||
savingPrice.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function charge() {
|
||||
if (!chargeAmount.value || chargeAmount.value < 1 || charging.value) return
|
||||
charging.value = true
|
||||
try {
|
||||
apply(
|
||||
await smsWalletApi.charge({
|
||||
amountToman: chargeAmount.value,
|
||||
note: chargeNote.value.trim() || undefined,
|
||||
}),
|
||||
)
|
||||
chargeAmount.value = null
|
||||
chargeNote.value = ''
|
||||
await refreshSummary()
|
||||
toast.success('کیف پول پیامک شارژ شد.')
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : 'شارژ کیف پول ناموفق بود.')
|
||||
} finally {
|
||||
charging.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function setBlocked(next: boolean) {
|
||||
if (!wallet.value || toggling.value) return
|
||||
const previous = wallet.value.blocked
|
||||
wallet.value.blocked = next
|
||||
toggling.value = true
|
||||
try {
|
||||
apply(await smsWalletApi.update({ blocked: next }))
|
||||
await refreshSummary()
|
||||
toast.success(next ? 'ارسال پیامک مسدود شد.' : 'ارسال پیامک فعال شد.')
|
||||
} catch (e) {
|
||||
wallet.value.blocked = previous
|
||||
toast.error(e instanceof Error ? e.message : 'تغییر وضعیت مسدودسازی ناموفق بود.')
|
||||
} finally {
|
||||
toggling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDeposit() {
|
||||
if (savingDeposit.value) return
|
||||
savingDeposit.value = true
|
||||
try {
|
||||
apply(
|
||||
await smsWalletApi.update({
|
||||
cardNumber: cardNumber.value,
|
||||
sheba: sheba.value,
|
||||
accountName: accountName.value,
|
||||
}),
|
||||
)
|
||||
await refreshSummary()
|
||||
toast.success('اطلاعات واریز ذخیره شد.')
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : 'ذخیره اطلاعات واریز ناموفق بود.')
|
||||
} finally {
|
||||
savingDeposit.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveThresholds() {
|
||||
if (yellowThreshold.value == null || redThreshold.value == null || savingThresholds.value) return
|
||||
savingThresholds.value = true
|
||||
try {
|
||||
apply(
|
||||
await smsWalletApi.update({
|
||||
yellowThreshold: yellowThreshold.value,
|
||||
redThreshold: redThreshold.value,
|
||||
}),
|
||||
)
|
||||
await refreshSummary()
|
||||
toast.success('آستانهها ذخیره شد.')
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : 'ذخیره آستانهها ناموفق بود.')
|
||||
} finally {
|
||||
savingThresholds.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function stat(row: SmsUsageStats | undefined) {
|
||||
return row ?? { messages: 0, parts: 0, costToman: 0 }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sms-page">
|
||||
<div class="admin-page-header">
|
||||
<h1 class="admin-page-header__title">تعرفه پیامکها</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageSkeleton v-if="loading" variant="form" :rows="8" />
|
||||
|
||||
<template v-else-if="wallet">
|
||||
<section class="sms-stats">
|
||||
<article class="sms-stat sms-stat--balance">
|
||||
<h2>مانده جاری</h2>
|
||||
<p><span>{{ formatNumber(stat(wallet.total).messages) }}</span> پیامک</p>
|
||||
<p><span>{{ formatNumber(stat(wallet.total).parts) }}</span> پارت</p>
|
||||
<p><span>{{ formatNumber(stat(wallet.total).costToman) }}</span> تومان</p>
|
||||
</article>
|
||||
<article class="sms-stat sms-stat--today">
|
||||
<h2>امروز</h2>
|
||||
<p><span>{{ formatNumber(stat(wallet.today).messages) }}</span> پیامک</p>
|
||||
<p><span>{{ formatNumber(stat(wallet.today).parts) }}</span> پارت</p>
|
||||
<p><span>{{ formatNumber(stat(wallet.today).costToman) }}</span> تومان</p>
|
||||
</article>
|
||||
<article class="sms-stat sms-stat--price">
|
||||
<h2>قیمت هر پارت</h2>
|
||||
<strong>{{ formatNumber(wallet.pricePerPart) }} تومان</strong>
|
||||
</article>
|
||||
<article class="sms-stat sms-stat--wallet">
|
||||
<h2>موجودی کیف پول</h2>
|
||||
<strong>{{ formatNumber(wallet.balanceToman) }} تومان</strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="admin-card sms-manage">
|
||||
<h2>مدیریت کیف پول</h2>
|
||||
<div class="sms-manage__row">
|
||||
<div class="sms-manage__tariff">
|
||||
<span>تعرفه پیامک</span>
|
||||
<strong>{{ formatNumber(wallet.pricePerPart) }}</strong>
|
||||
</div>
|
||||
<div class="sms-manage__price">
|
||||
<AdminCurrencyInput v-model="pricePerPart" :allow-empty="false" />
|
||||
<button
|
||||
type="button"
|
||||
class="admin-btn admin-btn--success"
|
||||
:disabled="savingPrice || !pricePerPart"
|
||||
@click="savePrice"
|
||||
>
|
||||
{{ savingPrice ? 'در حال ذخیره…' : 'ذخیره قیمت' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="sms-manage__charge">
|
||||
<AdminCurrencyInput v-model="chargeAmount" placeholder="مبلغ (تومان)" />
|
||||
<input
|
||||
v-model="chargeNote"
|
||||
class="admin-input"
|
||||
type="text"
|
||||
placeholder="توضیح (اختیاری)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="admin-btn admin-btn--success"
|
||||
:disabled="charging || !chargeAmount"
|
||||
@click="charge"
|
||||
>
|
||||
{{ charging ? 'در حال شارژ…' : 'شارژ کیف پول' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="sms-manage__hint">هر پیامک ۶۷ کاراکتر است. پیام ۱۴۰ کاراکتری ۳ پارت محاسبه میشود.</p>
|
||||
<div class="sms-block">
|
||||
<div>
|
||||
<strong>مسدودسازی ارسال پیامک</strong>
|
||||
<p>در حالت مسدود، ارسال پیامک متوقف میشود.</p>
|
||||
</div>
|
||||
<div class="sms-block__switch">
|
||||
<span>{{ wallet.blocked ? 'فعال' : 'خاموش' }}</span>
|
||||
<BaseSwitch
|
||||
:model-value="wallet.blocked"
|
||||
:loading="toggling"
|
||||
active-text="فعال"
|
||||
inactive-text="خاموش"
|
||||
@update:model-value="setBlocked"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="admin-card sms-deposit">
|
||||
<h2>اطلاعات واریز شارژ</h2>
|
||||
<p class="sms-deposit__hint">
|
||||
لطفا مبلغ مورد نظر خود جهت شارژ سامانه پیامکی را به شماره کارت یا حساب زیر واریز نمایید و تصویر فیش پرداختی را برای پشتیبانی ارسال فرمایید.
|
||||
</p>
|
||||
<label class="admin-form__field">
|
||||
<span>شماره کارت</span>
|
||||
<input v-model="cardNumber" class="admin-input" dir="ltr" inputmode="numeric" />
|
||||
</label>
|
||||
<label class="admin-form__field">
|
||||
<span>شماره شبا</span>
|
||||
<input v-model="sheba" class="admin-input" dir="ltr" />
|
||||
</label>
|
||||
<label class="admin-form__field">
|
||||
<span>به نام</span>
|
||||
<input v-model="accountName" class="admin-input" />
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="admin-btn admin-btn--success sms-deposit__save"
|
||||
:disabled="savingDeposit"
|
||||
@click="saveDeposit"
|
||||
>
|
||||
{{ savingDeposit ? 'در حال ذخیره…' : 'ذخیره اطلاعات واریز' }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="admin-card sms-thresholds">
|
||||
<h2>آستانههای رنگ موجودی</h2>
|
||||
<div class="sms-thresholds__row">
|
||||
<label class="admin-form__field">
|
||||
<span>آستانه زرد</span>
|
||||
<AdminCurrencyInput v-model="yellowThreshold" :allow-empty="false" />
|
||||
</label>
|
||||
<label class="admin-form__field">
|
||||
<span>آستانه قرمز</span>
|
||||
<AdminCurrencyInput v-model="redThreshold" :allow-empty="false" />
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="admin-btn admin-btn--success"
|
||||
:disabled="savingThresholds"
|
||||
@click="saveThresholds"
|
||||
>
|
||||
{{ savingThresholds ? 'در حال ذخیره…' : 'ذخیره آستانهها' }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sms-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sms-stat {
|
||||
border-radius: 16px;
|
||||
padding: 16px 18px;
|
||||
min-height: 112px;
|
||||
}
|
||||
|
||||
.sms-stat h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.sms-stat p {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted, #64748b);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.sms-stat p span,
|
||||
.sms-stat strong {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.sms-stat--balance {
|
||||
background: #f3e8ff;
|
||||
color: #6d28d9;
|
||||
}
|
||||
|
||||
.sms-stat--today {
|
||||
background: #fff7ed;
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.sms-stat--price {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.sms-stat--price strong,
|
||||
.sms-stat--wallet strong {
|
||||
display: block;
|
||||
margin-top: 18px;
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.sms-stat--wallet {
|
||||
background: #fffbeb;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.sms-manage,
|
||||
.sms-deposit,
|
||||
.sms-thresholds {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sms-manage h2,
|
||||
.sms-deposit h2,
|
||||
.sms-thresholds h2 {
|
||||
margin: 0 0 14px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.sms-manage__row,
|
||||
.sms-thresholds__row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sms-manage__tariff,
|
||||
.sms-manage__price,
|
||||
.sms-manage__charge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sms-manage__price :deep(input),
|
||||
.sms-manage__charge :deep(input),
|
||||
.sms-manage__charge .admin-input,
|
||||
.sms-thresholds__row :deep(input) {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.sms-manage__tariff strong {
|
||||
min-width: 72px;
|
||||
text-align: center;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.sms-manage__charge {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.sms-manage__hint,
|
||||
.sms-deposit__hint {
|
||||
margin: 12px 0 0;
|
||||
color: var(--color-text-muted, #64748b);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.sms-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: #ecfdf3;
|
||||
}
|
||||
|
||||
.sms-block p {
|
||||
margin: 4px 0 0;
|
||||
color: #067647;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.sms-block__switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #067647;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sms-deposit__save {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.sms-thresholds__row .admin-btn {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.admin-form__field > span {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.sms-stats {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.sms-manage__charge {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user