added table
This commit is contained in:
Vendored
+1
@@ -88,6 +88,7 @@ declare module '@vue/runtime-core' {
|
||||
BudgetSelect: typeof import('./src/components/BudgetSelect.vue')['default']
|
||||
CustomerImportProgress: typeof import('./src/components/CustomerImportProgress.vue')['default']
|
||||
CustomTag: typeof import('./src/components/CustomTag.vue')['default']
|
||||
GroupSmsSendProgress: typeof import('./src/components/GroupSmsSendProgress.vue')['default']
|
||||
PargarRetryModal: typeof import('./src/components/PargarRetryModal.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<AConfigProvider direction="rtl">
|
||||
<RouterView />
|
||||
<CustomerImportProgress />
|
||||
<GroupSmsSendProgress />
|
||||
</AConfigProvider>
|
||||
</template>
|
||||
|
||||
@@ -11,6 +12,7 @@ import { message } from 'ant-design-vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { initDarkmode } from '@core/stores/darkmode'
|
||||
import CustomerImportProgress from '@/components/CustomerImportProgress.vue'
|
||||
import GroupSmsSendProgress from '@/components/GroupSmsSendProgress.vue'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { bootstrapAdminChat, useAdminChatService } from '@/utils/adminChat'
|
||||
import { supportChatMenuEnabledSetting } from '@/utils/config'
|
||||
|
||||
@@ -6,6 +6,9 @@ import type { AddId, Customer } from '@/model/customer'
|
||||
export const getOrderTable = async (params: TableInput) =>
|
||||
await useAxios.post<Array<Order>>('/admin/order/table', params)
|
||||
|
||||
export const getColleagueOrderTable = async (params: TableInput) =>
|
||||
await useAxios.post<TableOutput<Order>>('/admin/order/colleague/table', params)
|
||||
|
||||
export const createOrder = async (obj: Order) =>
|
||||
await useAxios.post<AddId>(`/admin/single/order`, obj, { meta: { silent: true } })
|
||||
|
||||
|
||||
@@ -0,0 +1,714 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="gsp-fade">
|
||||
<div
|
||||
v-if="store.visible && !store.minimized"
|
||||
class="gsp-overlay"
|
||||
@click.self="store.active ? store.minimize() : null"
|
||||
>
|
||||
<div class="gsp-modal" :class="store.finished ? `gsp-modal--${outcomeInfo.tone}` : null">
|
||||
<div class="gsp-modal__head">
|
||||
<div class="gsp-modal__title">
|
||||
<Icon :icon="headIcon" :size="18" />
|
||||
<span>{{ headTitle }}</span>
|
||||
</div>
|
||||
<div class="gsp-modal__head-actions">
|
||||
<button
|
||||
v-if="store.active"
|
||||
type="button"
|
||||
class="gsp-icon-btn"
|
||||
title="کوچک کردن"
|
||||
@click="store.minimize()"
|
||||
>
|
||||
<Icon icon="vuesax-linear:minus" :size="18" />
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="gsp-icon-btn"
|
||||
title="بستن"
|
||||
@click="store.dismiss()"
|
||||
>
|
||||
<Icon icon="vuesax-linear:close-circle" :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="store.active" class="gsp-modal__body">
|
||||
<a-alert
|
||||
class="gsp-warn"
|
||||
type="warning"
|
||||
show-icon
|
||||
message="پنل را نبندید و صفحه را رفرش نکنید؛ ارسال قطع میشود."
|
||||
/>
|
||||
|
||||
<div class="gsp-counter">
|
||||
<span class="gsp-counter__done">{{ store.processed }}</span>
|
||||
<span class="gsp-counter__sep">/</span>
|
||||
<span class="gsp-counter__total">{{ store.total }}</span>
|
||||
</div>
|
||||
<div class="gsp-counter__label">در حال ارسال پیامک گروهی (۱۰تایی)…</div>
|
||||
|
||||
<div class="gsp-track">
|
||||
<div class="gsp-track__fill" :style="{ width: store.percent + '%' }" />
|
||||
</div>
|
||||
<div class="gsp-percent">{{ store.percent }}٪</div>
|
||||
|
||||
<div class="gsp-current">
|
||||
<a-spin size="small" />
|
||||
<span class="gsp-current__name">{{ store.currentLabel || '...' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="gsp-stats">
|
||||
<div class="gsp-stat gsp-stat--ok">
|
||||
<Icon icon="vuesax-linear:tick-circle" :size="14" />
|
||||
{{ store.success }} موفق
|
||||
</div>
|
||||
<div v-if="store.failed" class="gsp-stat gsp-stat--fail">
|
||||
<Icon icon="vuesax-linear:close-circle" :size="14" />
|
||||
{{ store.failed }} ناموفق
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="gsp-modal__body gsp-report">
|
||||
<div class="gsp-report__hero" :class="`gsp-report__hero--${outcomeInfo.tone}`">
|
||||
<Icon :icon="outcomeInfo.icon" :size="34" />
|
||||
</div>
|
||||
<div class="gsp-report__title">{{ outcomeInfo.text }}</div>
|
||||
<div class="gsp-report__subtitle">
|
||||
از مجموع {{ store.total }} شماره، {{ store.processed }} مورد پردازش شد
|
||||
</div>
|
||||
|
||||
<div class="gsp-report__cards">
|
||||
<div class="gsp-rcard gsp-rcard--ok">
|
||||
<Icon icon="vuesax-linear:tick-circle" :size="20" />
|
||||
<div class="gsp-rcard__num">{{ store.success }}</div>
|
||||
<div class="gsp-rcard__label">با موفقیت ارسال شد</div>
|
||||
</div>
|
||||
<div class="gsp-rcard" :class="store.failed ? 'gsp-rcard--fail' : 'gsp-rcard--muted'">
|
||||
<Icon icon="vuesax-linear:close-circle" :size="20" />
|
||||
<div class="gsp-rcard__num">{{ store.failed }}</div>
|
||||
<div class="gsp-rcard__label">ناموفق / خطا</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="store.errors.length" class="gsp-report__errors">
|
||||
<div class="gsp-report__errors-head">
|
||||
<Icon icon="vuesax-linear:close-circle" :size="14" />
|
||||
جزئیات دستههای ناموفق ({{ store.errors.length }})
|
||||
</div>
|
||||
<div class="gsp-report__errors-list">
|
||||
<div v-for="e in store.errors" :key="`b-${e.batch}`" class="gsp-erow">
|
||||
<span class="gsp-erow__name">دسته {{ e.batch }} ({{ e.numbers.length }} شماره)</span>
|
||||
<span class="gsp-erow__msg">{{ e.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gsp-modal__foot">
|
||||
<a-button v-if="store.active" danger size="small" @click="store.cancel()">
|
||||
توقف
|
||||
</a-button>
|
||||
<a-button v-if="store.active" size="small" @click="store.minimize()">
|
||||
کوچک کردن
|
||||
</a-button>
|
||||
<a-button v-if="store.finished" type="primary" size="small" @click="store.dismiss()">
|
||||
بستن گزارش
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<Transition name="gsp-slide">
|
||||
<button
|
||||
v-if="store.visible && store.minimized"
|
||||
type="button"
|
||||
class="gsp-mini"
|
||||
:class="store.finished ? `gsp-mini--${outcomeInfo.tone}` : null"
|
||||
@click="store.expand()"
|
||||
>
|
||||
<span class="gsp-mini__ring">
|
||||
<svg viewBox="0 0 36 36" class="gsp-mini__svg">
|
||||
<circle class="gsp-mini__bg" cx="18" cy="18" r="15.5" />
|
||||
<circle class="gsp-mini__fg" cx="18" cy="18" r="15.5" :style="ringStyle" />
|
||||
</svg>
|
||||
<span class="gsp-mini__pct">
|
||||
<template v-if="store.finished">
|
||||
<Icon :icon="outcomeInfo.icon" :size="16" />
|
||||
</template>
|
||||
<template v-else>{{ store.percent }}</template>
|
||||
</span>
|
||||
</span>
|
||||
<span class="gsp-mini__text">
|
||||
<span class="gsp-mini__title">
|
||||
<template v-if="store.finished">{{ outcomeInfo.text }}</template>
|
||||
<template v-else>ارسال پیامک گروهی</template>
|
||||
</span>
|
||||
<span class="gsp-mini__count">
|
||||
<template v-if="store.finished">{{ store.success }} موفق · {{ store.failed }} خطا</template>
|
||||
<template v-else>{{ store.processed }} از {{ store.total }} · پنل را نبندید</template>
|
||||
</span>
|
||||
</span>
|
||||
<span class="gsp-mini__bar">
|
||||
<span class="gsp-mini__bar-fill" :style="{ width: store.percent + '%' }" />
|
||||
</span>
|
||||
</button>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, watch } from 'vue'
|
||||
import Icon from '@core/components/BaseIcon/src/Icon.vue'
|
||||
import { useGroupSmsSendStore } from '@/store/groupSmsSendStore'
|
||||
|
||||
const store = useGroupSmsSendStore()
|
||||
|
||||
const OUTCOME_MAP = {
|
||||
success: {
|
||||
icon: 'vuesax-linear:tick-circle',
|
||||
text: 'ارسال پیامک تکمیل شد',
|
||||
tone: 'ok' as const
|
||||
},
|
||||
error: {
|
||||
icon: 'vuesax-linear:close-circle',
|
||||
text: 'ارسال با خطا مواجه شد',
|
||||
tone: 'fail' as const
|
||||
},
|
||||
partial: {
|
||||
icon: 'vuesax-linear:danger',
|
||||
text: 'بعضی دستهها با خطا مواجه شد',
|
||||
tone: 'warn' as const
|
||||
},
|
||||
canceled: {
|
||||
icon: 'vuesax-linear:info-circle',
|
||||
text: 'ارسال متوقف شد',
|
||||
tone: 'warn' as const
|
||||
},
|
||||
running: {
|
||||
icon: 'vuesax-linear:sms',
|
||||
text: 'ارسال پیامک گروهی',
|
||||
tone: 'ok' as const
|
||||
}
|
||||
}
|
||||
|
||||
const outcomeInfo = computed(() => OUTCOME_MAP[store.outcome])
|
||||
const headIcon = computed(() => (store.finished ? outcomeInfo.value.icon : 'vuesax-linear:sms'))
|
||||
const headTitle = computed(() =>
|
||||
store.finished ? 'گزارش ارسال پیامک گروهی' : 'ارسال پیامک گروهی'
|
||||
)
|
||||
|
||||
const RING_CIRC = 2 * Math.PI * 15.5
|
||||
const ringStyle = computed(() => ({
|
||||
strokeDasharray: `${RING_CIRC}`,
|
||||
strokeDashoffset: `${RING_CIRC * (1 - store.percent / 100)}`
|
||||
}))
|
||||
|
||||
function onBeforeUnload(e: BeforeUnloadEvent) {
|
||||
if (!store.active) return
|
||||
e.preventDefault()
|
||||
e.returnValue = ''
|
||||
}
|
||||
|
||||
watch(
|
||||
() => store.active,
|
||||
(active) => {
|
||||
if (active) window.addEventListener('beforeunload', onBeforeUnload)
|
||||
else window.removeEventListener('beforeunload', onBeforeUnload)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('beforeunload', onBeforeUnload)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.gsp-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.gsp-modal {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-height: min(90vh, 640px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 16px;
|
||||
background: var(--admin-bg-surface);
|
||||
border: 1px solid var(--admin-border-strong);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
||||
overflow: hidden;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
background: linear-gradient(135deg, #0284c7 0%, #0ea5e9 55%, #6366f1 100%);
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&--ok .gsp-modal__head {
|
||||
background: linear-gradient(135deg, #059669 0%, #10b981 100%);
|
||||
}
|
||||
|
||||
&--warn .gsp-modal__head {
|
||||
background: linear-gradient(135deg, #d97706 0%, #f59e0b 100%);
|
||||
}
|
||||
|
||||
&--fail .gsp-modal__head {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&__head-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 20px 18px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 16px 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.gsp-warn {
|
||||
width: 100%;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.gsp-icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.32);
|
||||
}
|
||||
}
|
||||
|
||||
.gsp-counter {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
|
||||
&__done {
|
||||
font-size: 38px;
|
||||
color: #0284c7;
|
||||
}
|
||||
&__sep {
|
||||
font-size: 24px;
|
||||
color: var(--admin-text-muted);
|
||||
}
|
||||
&__total {
|
||||
font-size: 24px;
|
||||
color: var(--admin-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.gsp-counter__label {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--admin-text-secondary);
|
||||
}
|
||||
|
||||
.gsp-track {
|
||||
width: 100%;
|
||||
height: 9px;
|
||||
margin-top: 16px;
|
||||
border-radius: 99px;
|
||||
background: var(--admin-bg-muted);
|
||||
overflow: hidden;
|
||||
|
||||
&__fill {
|
||||
height: 100%;
|
||||
border-radius: 99px;
|
||||
background: linear-gradient(90deg, #0284c7, #6366f1);
|
||||
transition: width 0.35s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.gsp-percent {
|
||||
align-self: flex-end;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--admin-text-secondary);
|
||||
}
|
||||
|
||||
.gsp-current {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
max-width: 100%;
|
||||
|
||||
&__name {
|
||||
font-size: 12px;
|
||||
color: var(--admin-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.gsp-stats {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.gsp-stat {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 3px 12px;
|
||||
border-radius: 99px;
|
||||
|
||||
&--ok {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #059669;
|
||||
}
|
||||
&--fail {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
.gsp-report {
|
||||
padding-top: 22px;
|
||||
|
||||
&__hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
|
||||
&--ok {
|
||||
background: rgba(16, 185, 129, 0.14);
|
||||
color: #059669;
|
||||
}
|
||||
&--warn {
|
||||
background: rgba(245, 158, 11, 0.16);
|
||||
color: #d97706;
|
||||
}
|
||||
&--fail {
|
||||
background: rgba(239, 68, 68, 0.13);
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-top: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--admin-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--admin-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
&__errors {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__errors-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #dc2626;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&__errors-list {
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--admin-border);
|
||||
background: var(--admin-bg-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.gsp-rcard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 14px 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&__num {
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
&__label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&--ok {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: rgba(16, 185, 129, 0.25);
|
||||
color: #059669;
|
||||
}
|
||||
&--fail {
|
||||
background: rgba(239, 68, 68, 0.09);
|
||||
border-color: rgba(239, 68, 68, 0.22);
|
||||
color: #dc2626;
|
||||
}
|
||||
&--muted {
|
||||
background: var(--admin-bg-muted);
|
||||
border-color: var(--admin-border);
|
||||
color: var(--admin-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.gsp-erow {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid var(--admin-border);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: 700;
|
||||
color: var(--admin-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
&__msg {
|
||||
color: #dc2626;
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.gsp-mini {
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 16px 8px 10px;
|
||||
border: 1px solid var(--admin-border-strong);
|
||||
border-radius: 99px;
|
||||
background: var(--admin-bg-surface);
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.22);
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
top: 58px;
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateX(-50%) translateY(1px);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
&__ring {
|
||||
position: relative;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
&__bg {
|
||||
fill: none;
|
||||
stroke: var(--admin-bg-muted);
|
||||
stroke-width: 3.5;
|
||||
}
|
||||
|
||||
&__fg {
|
||||
fill: none;
|
||||
stroke: #0284c7;
|
||||
stroke-width: 3.5;
|
||||
stroke-linecap: round;
|
||||
transition: stroke-dashoffset 0.35s ease;
|
||||
}
|
||||
|
||||
&__pct {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: #0284c7;
|
||||
}
|
||||
|
||||
&__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.3;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--admin-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 11px;
|
||||
color: var(--admin-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
bottom: 3px;
|
||||
height: 2px;
|
||||
border-radius: 99px;
|
||||
background: var(--admin-bg-muted);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__bar-fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: 99px;
|
||||
background: #0284c7;
|
||||
transition: width 0.35s ease;
|
||||
}
|
||||
|
||||
&--ok {
|
||||
.gsp-mini__fg,
|
||||
.gsp-mini__bar-fill {
|
||||
stroke: #10b981;
|
||||
background: #10b981;
|
||||
}
|
||||
.gsp-mini__pct {
|
||||
color: #059669;
|
||||
}
|
||||
}
|
||||
|
||||
&--warn {
|
||||
.gsp-mini__fg,
|
||||
.gsp-mini__bar-fill {
|
||||
stroke: #f59e0b;
|
||||
background: #f59e0b;
|
||||
}
|
||||
.gsp-mini__pct {
|
||||
color: #d97706;
|
||||
}
|
||||
}
|
||||
|
||||
&--fail {
|
||||
.gsp-mini__fg,
|
||||
.gsp-mini__bar-fill {
|
||||
stroke: #ef4444;
|
||||
background: #ef4444;
|
||||
}
|
||||
.gsp-mini__pct {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gsp-fade-enter-active,
|
||||
.gsp-fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.gsp-fade-enter-from,
|
||||
.gsp-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.gsp-slide-enter-active,
|
||||
.gsp-slide-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
.gsp-slide-enter-from,
|
||||
.gsp-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-12px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="min-w-0 w-full max-w-full overflow-x-hidden">
|
||||
<BasicShowContext title="تراکنش همکار">
|
||||
<div class="min-w-0 w-full max-w-full overflow-x-auto">
|
||||
<BaseTable v-bind="tblProps" @register="submit" />
|
||||
</div>
|
||||
</BasicShowContext>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { getColleagueOrderTable } from '@/api/order'
|
||||
import { getColleagueOrderColumns } from '@/pages/menuPages/colleagueOrders/hooks/colleagueOrderColumns'
|
||||
import useBaseTable from '@core/components/BaseTable/hooks/useBaseTable'
|
||||
import type { TableInput } from '@core/type/baseTable'
|
||||
|
||||
const [submit, reload] = useBaseTable()
|
||||
|
||||
const tblProps = ref({
|
||||
columns: getColleagueOrderColumns(),
|
||||
api: (params: TableInput) => getColleagueOrderTable(params)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
fetch: reload
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,134 @@
|
||||
import type { BaseTableColumn } from '@core/type/baseTable'
|
||||
import { toJalali } from '@core/utils/useDataTime'
|
||||
import CustomTag from '@/components/CustomTag.vue'
|
||||
import { usePrice } from '@core/utils/usePrice'
|
||||
|
||||
export function getColleagueOrderColumns(): BaseTableColumn[] {
|
||||
return [
|
||||
{
|
||||
title: 'ردیف',
|
||||
dataIndex: 'id',
|
||||
key: 'row',
|
||||
filterable: false,
|
||||
sortable: false,
|
||||
customRender: (record: any) => record.index + 1
|
||||
},
|
||||
{
|
||||
title: 'کد سفارش',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
type: 'Number'
|
||||
},
|
||||
{
|
||||
title: 'کد همکار',
|
||||
dataIndex: 'provider_id',
|
||||
key: 'provider_id',
|
||||
type: 'Number',
|
||||
customRender: ({ record }) => record.provider_id ?? '-'
|
||||
},
|
||||
{
|
||||
title: 'نوع',
|
||||
dataIndex: 'direction',
|
||||
key: 'direction',
|
||||
filterable: false,
|
||||
customRender: ({ record }) => {
|
||||
if (record.direction === 2) {
|
||||
return (
|
||||
<div class="flex justify-center">
|
||||
<CustomTag textColor="#C53030" bgColor="#FECACA">
|
||||
فروش
|
||||
</CustomTag>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div class="flex justify-center">
|
||||
<CustomTag textColor="#25855A" bgColor="#E3F4E9">
|
||||
خرید
|
||||
</CustomTag>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'مشتری',
|
||||
dataIndex: 'customer.name',
|
||||
key: 'customer.name',
|
||||
type: 'Text'
|
||||
},
|
||||
{
|
||||
title: 'مقدار مشتری (گرم)',
|
||||
dataIndex: 'customer_amount',
|
||||
key: 'customer_amount',
|
||||
type: 'Number',
|
||||
filterable: false,
|
||||
customRender: ({ record }) => record.customer_amount ?? record.amount ?? '-'
|
||||
},
|
||||
{
|
||||
title: 'مقدار نزد همکار (گرم)',
|
||||
dataIndex: 'colleague_amount',
|
||||
key: 'colleague_amount',
|
||||
type: 'Number',
|
||||
filterable: false,
|
||||
customRender: ({ record }) => record.colleague_amount ?? record.amount ?? '-'
|
||||
},
|
||||
{
|
||||
title: 'کارمزد',
|
||||
dataIndex: 'service_charge',
|
||||
key: 'service_charge',
|
||||
type: 'Number',
|
||||
customRender: ({ record }) => {
|
||||
if (record.fee_asset === 'rial') {
|
||||
return usePrice(record.service_charge_price ?? 0, true, '')
|
||||
}
|
||||
const gold = record.service_charge ?? 0
|
||||
return `${gold} گرم`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'قیمت مشتری (ریال)',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
type: 'Text',
|
||||
customRender: ({ record }) => usePrice(record.price ?? 0, true, '')
|
||||
},
|
||||
{
|
||||
title: 'مبلغ مشتری (ریال)',
|
||||
dataIndex: 'total_price',
|
||||
key: 'total_price',
|
||||
type: 'Text',
|
||||
customRender: ({ record }) => usePrice(record.total_price ?? 0, true, '')
|
||||
},
|
||||
{
|
||||
title: 'نرخ همکار (ریال/گرم)',
|
||||
dataIndex: 'colleague_unit_price',
|
||||
key: 'colleague_unit_price',
|
||||
type: 'Text',
|
||||
filterable: false,
|
||||
customRender: ({ record }) =>
|
||||
usePrice(record.colleague_unit_price ?? record.total_price_montzar ?? 0, true, '')
|
||||
},
|
||||
{
|
||||
title: 'مبلغ نزد همکار (ریال)',
|
||||
dataIndex: 'colleague_total_price',
|
||||
key: 'colleague_total_price',
|
||||
type: 'Text',
|
||||
filterable: false,
|
||||
customRender: ({ record }) => usePrice(record.colleague_total_price ?? 0, true, '')
|
||||
},
|
||||
{
|
||||
title: 'سود کل (ریال)',
|
||||
dataIndex: 'total_fee',
|
||||
key: 'total_fee',
|
||||
type: 'Text',
|
||||
customRender: ({ record }) => usePrice(record.total_fee ?? 0, true, '')
|
||||
},
|
||||
{
|
||||
title: 'تاریخ',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
type: 'DateTime',
|
||||
customRender: (record: any) => toJalali(record.record.created_at)
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div>
|
||||
<BasicMainCard>
|
||||
<div class="flex h-full w-full min-w-0 max-w-full flex-col overflow-hidden">
|
||||
<tbl ref="table" class="min-h-0 flex-1" />
|
||||
</div>
|
||||
</BasicMainCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import tbl from './components/table.vue'
|
||||
|
||||
const table = ref<InstanceType<typeof tbl> | null>(null)
|
||||
</script>
|
||||
@@ -202,7 +202,7 @@ import SettingValueField from './SettingValueField.vue'
|
||||
import SettingCard from './SettingCard.vue'
|
||||
import { refreshAppModeMenu } from '@/router'
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings } from '@/utils/config'
|
||||
import { applyBannerEnabledFromRows, applyTrackingSystemsEnabledFromRows, applyConditionalOrderEnabledFromRows, applyProductFundsEnabledFromRows, applyPriceTriggerEnabledFromRows, applyAppNotificationEnabledFromRows, applyPriceProviderFromRows, applyPriceChannelAccessFromRows, applySupportChatMenuEnabledFromRows, applyGroupSmsFromRows, setBannerEnabledFromSettings, setConditionalOrderEnabledFromSettings, setProductFundsEnabledFromSettings, setPriceTriggerEnabledFromSettings, setAppNotificationEnabledFromSettings, setPriceProviderFromSettings, setPriceChannelAccessFromSettings, setStoreModeFromSettings, setTrackingSystemsEnabledFromSettings, setGroupSmsEnabledFromSettings } from '@/utils/config'
|
||||
import {
|
||||
EDITABLE_SETTING_GROUPS,
|
||||
SYSTEM_SETTING_GROUPS,
|
||||
@@ -484,11 +484,13 @@ async function initConfig() {
|
||||
applyPriceProviderFromRows(baseData.value)
|
||||
applyPriceChannelAccessFromRows(baseData.value)
|
||||
applySupportChatMenuEnabledFromRows(baseData.value)
|
||||
applyGroupSmsFromRows(baseData.value)
|
||||
// در صفحهٔ عادی price_provider و show_to_users_chat و price_channel_access (can_edit=0) نیستند — از کل تنظیمات بخوان
|
||||
if (
|
||||
!baseData.value.some((row) => row.key === 'price_provider') ||
|
||||
!baseData.value.some((row) => row.key === 'show_to_users_chat') ||
|
||||
!baseData.value.some((row) => row.key === 'price_channel_access')
|
||||
!baseData.value.some((row) => row.key === 'price_channel_access') ||
|
||||
!baseData.value.some((row) => row.key === 'group_sms')
|
||||
) {
|
||||
try {
|
||||
const { getConfiguration } = await import('@/api/configuration')
|
||||
@@ -497,6 +499,7 @@ async function initConfig() {
|
||||
applyPriceProviderFromRows(rows)
|
||||
applySupportChatMenuEnabledFromRows(rows)
|
||||
applyPriceChannelAccessFromRows(rows)
|
||||
applyGroupSmsFromRows(rows)
|
||||
} catch {
|
||||
/* کش قبلی میماند */
|
||||
}
|
||||
@@ -566,6 +569,9 @@ function syncMenuFlagsFromSetting(key: string | undefined, value: string) {
|
||||
if (key === 'price_channel_access') {
|
||||
setPriceChannelAccessFromSettings(isSwitchOn(value))
|
||||
}
|
||||
if (key === 'group_sms') {
|
||||
setGroupSmsEnabledFromSettings(isSwitchOn(value))
|
||||
}
|
||||
}
|
||||
|
||||
function onValueChange(row: Const, value: string) {
|
||||
|
||||
@@ -155,7 +155,6 @@ export const EDITABLE_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
icon: 'vuesax-linear:notification',
|
||||
match: (k) =>
|
||||
k === 'charge_notif' ||
|
||||
k === 'group_sms' ||
|
||||
k === 'register_referral_sms_to_referrer' ||
|
||||
/^sms_notif_/.test(k)
|
||||
},
|
||||
@@ -180,10 +179,16 @@ export const SYSTEM_SETTING_GROUPS: SettingGroupDef[] = [
|
||||
title: 'دسترسی مشتری به قابلیتها',
|
||||
icon: 'vuesax-linear:people',
|
||||
match: (k) =>
|
||||
/^(enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|banner_enabled|jibit_enabled|customer_direct_sms|has_page_earn_money|group_sms|tracking_systems_enabled|permision__enabled|show_to_users_chat)$/.test(
|
||||
/^(enable_conditional_order|enable_product_funds|has_page_price_trigger|has_page_notification|has_page_gift_card|enable_customer_level_profit|banner_enabled|jibit_enabled|customer_direct_sms|has_page_earn_money|tracking_systems_enabled|permision__enabled|show_to_users_chat)$/.test(
|
||||
k
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'limits',
|
||||
title: 'محدودیتها',
|
||||
icon: 'vuesax-linear:lock',
|
||||
match: (k) => k === 'group_sms'
|
||||
},
|
||||
{
|
||||
id: 'support_chat',
|
||||
title: 'چت پشتیبانی',
|
||||
|
||||
@@ -824,7 +824,7 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
|
||||
sms_notif_support_admin:
|
||||
'روشن: وقتی مشتری اولین پیام خواندهنشده را در چت پشتیبانی میفرستد و ادمین همان گفتگو را باز و visible ندارد، پیامک به ادمینهای دارای «ارسال پیامک» میرود. خاموش: اعلان پیامک چت نمیرود.',
|
||||
group_sms:
|
||||
'روشن: امکان ارسال پیامک گروهی از پنل ادمین فعال است. خاموش: ارسال پیامک گروهی غیرفعال است.',
|
||||
'روشن: دکمه ارسال پیامک گروهی در پنل ادمین فعال است (ارسال ۱۰تایی). خاموش: ارسال پیامک گروهی غیرفعال است. فقط از تنظیمات پیشرفته.',
|
||||
profit_gold_api_enabled:
|
||||
'روشن: محاسبه سود طلا از API خارجی فعال است. خاموش: محاسبه سود از API خارجی انجام نمیشود (حالت پیشفرض اکثر نصبها).',
|
||||
main_wallet_products:
|
||||
|
||||
@@ -24,7 +24,12 @@ import BasicMenu from '@/components/BasicMenu/index.vue'
|
||||
import { useRouterStore } from '@core/stores/router'
|
||||
import { get } from 'lodash'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { conditionalOrderEnabledSetting, initWalletSettingsFromApi, priceTriggerEnabledSetting } from '@/utils/config'
|
||||
import {
|
||||
conditionalOrderEnabledSetting,
|
||||
initWalletSettingsFromApi,
|
||||
priceProviderSetting,
|
||||
priceTriggerEnabledSetting
|
||||
} from '@/utils/config'
|
||||
|
||||
const routerStore = useRouterStore()
|
||||
const route = useRoute()
|
||||
@@ -40,6 +45,7 @@ const menu = computed(() => {
|
||||
.filter((child) => !child.meta?.hideMenu)
|
||||
.filter((child) => child.name !== 'conditionalOrders' || conditionalOrderEnabledSetting.value)
|
||||
.filter((child) => child.name !== 'priceTriggers' || priceTriggerEnabledSetting.value)
|
||||
.filter((child) => child.name !== 'colleagueOrdersMain' || priceProviderSetting.value === 'montzar')
|
||||
.sort((a, b) => (Number(a.meta?.order) || 99) - (Number(b.meta?.order) || 99))
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -42,13 +42,13 @@
|
||||
rowKey="mobile"
|
||||
:row-selection="{ selectedRowKeys: form.numbers, onChange: onSelectChange }"
|
||||
:columns="columns"
|
||||
:scroll="{ y: '20vh', x: 500 }"
|
||||
:scroll="{ y: 280, x: 500 }"
|
||||
:pagination="false"
|
||||
:data-source="groupNumbers"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="md:hidden max-h-48 overflow-y-auto rounded-xl border border-gray-200 bg-gray-50 p-2 space-y-1"
|
||||
class="md:hidden max-h-64 overflow-y-auto rounded-xl border border-gray-200 bg-gray-50 p-2 space-y-1"
|
||||
>
|
||||
<div v-if="customersLoading" class="flex justify-center py-6">
|
||||
<a-spin size="small" />
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<template>
|
||||
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
|
||||
<BasicShowContext title="لیست پیامک ها">
|
||||
<!-- <template v-if="!mobile && isMobileScreen" #extra>
|
||||
<template v-if="groupSmsEnabled && !mobile && isMobileScreen" #extra>
|
||||
<BaseButton type="primary" size="small" icon="vuesax-linear:sms" @click="emits('add')">
|
||||
ارسال پیامک
|
||||
</BaseButton>
|
||||
</template>-->
|
||||
</template>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<BaseTable v-if="!isMobileScreen" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
|
||||
<!-- <template #extraHeader>
|
||||
<BaseButton v-if="!mobile" type="primary" size="small" icon="vuesax-linear:sms" @click="emits('add')">
|
||||
ارسال پیامک
|
||||
</BaseButton>
|
||||
</template>-->
|
||||
<template #action="{ column, record }">
|
||||
<!-- <div class="gap-2 flex justify-center">
|
||||
<template v-if="groupSmsEnabled" #extraHeader>
|
||||
<BaseButton v-if="!mobile" type="primary" size="small" icon="vuesax-linear:sms" @click="emits('add')">
|
||||
ارسال پیامک
|
||||
</BaseButton>
|
||||
</template>
|
||||
<template #action="{ column, record }">
|
||||
<!-- <div class="gap-2 flex justify-center">
|
||||
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
|
||||
<template #title>
|
||||
<span>مشخصات</span>
|
||||
@@ -23,10 +23,10 @@
|
||||
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
|
||||
</a-tooltip>
|
||||
</div>-->
|
||||
</template>
|
||||
</BaseTable>
|
||||
</template>
|
||||
</BaseTable>
|
||||
|
||||
<SmsMobileCardList v-else ref="mobileListRef" class="min-h-0 flex-1" :mobile="mobile" />
|
||||
<SmsMobileCardList v-else ref="mobileListRef" class="min-h-0 flex-1" :mobile="mobile" />
|
||||
</div>
|
||||
</BasicShowContext>
|
||||
</div>
|
||||
@@ -39,6 +39,7 @@ import { cols } from '../hooks/smsColumns'
|
||||
import { getSMSTable } from '@/api/sms'
|
||||
import SmsMobileCardList from './SmsMobileCardList.vue'
|
||||
import { isMobile as isMobileScreen } from '@core/utils/is'
|
||||
import { groupSmsEnabledSetting, initWalletSettingsFromApi } from '@/utils/config'
|
||||
|
||||
interface Prop {
|
||||
mobile?: number | string
|
||||
@@ -49,6 +50,11 @@ const emits = defineEmits(['delete', 'edit', 'add', 'info'])
|
||||
|
||||
const [submit, reload] = useBaseTable()
|
||||
const mobileListRef = ref<null | { fetch: () => Promise<void> }>(null)
|
||||
const groupSmsEnabled = groupSmsEnabledSetting
|
||||
|
||||
onMounted(() => {
|
||||
void initWalletSettingsFromApi()
|
||||
})
|
||||
|
||||
const tblProps = ref<any>({
|
||||
columns: cols,
|
||||
|
||||
@@ -15,29 +15,39 @@
|
||||
:showButton="true"
|
||||
:showCancel="true"
|
||||
:showOk="true"
|
||||
ok-text="ارسال"
|
||||
ok-text="شروع ارسال"
|
||||
cancel-text="انصراف"
|
||||
@ok="ok"
|
||||
@close="closeSmsSheet"
|
||||
@cancel="closeSmsSheet"
|
||||
>
|
||||
<div v-if="isMobile" class="mb-3 text-base font-700">ارسال پیامک</div>
|
||||
<SmsEditorFormBody ref="smsEditorRef" :form="form" :group-items="groupItems" :loading="loading" />
|
||||
<a-alert
|
||||
class="mb-3"
|
||||
type="warning"
|
||||
show-icon
|
||||
message="ارسال ۱۰تایی انجام میشود. تا پایان کار پنل را نبندید و صفحه را رفرش نکنید."
|
||||
/>
|
||||
<div class="sms-editor-scroll">
|
||||
<SmsEditorFormBody ref="smsEditorRef" :form="form" :group-items="groupItems" :loading="loading" />
|
||||
</div>
|
||||
</BasicActionSheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import tbl from './components/table.vue'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import BasicActionSheet from '@/components/BasicActionSheet.vue'
|
||||
import SmsEditorFormBody from './components/SmsEditorFormBody.vue'
|
||||
import { message } from 'ant-design-vue/es'
|
||||
import type { FunctionItem } from '@/model/Item'
|
||||
import { createSMS, getGroupsList } from '@/api/sms'
|
||||
import { getGroupsList } from '@/api/sms'
|
||||
import type { Sms } from '@/model/sms'
|
||||
import { isMobile } from '@core/utils/is'
|
||||
import { groupSmsEnabledSetting, initWalletSettingsFromApi } from '@/utils/config'
|
||||
import { useGroupSmsSendStore } from '@/store/groupSmsSendStore'
|
||||
|
||||
const loading = ref<boolean>(false)
|
||||
const groupItems = ref<FunctionItem[]>()
|
||||
@@ -46,6 +56,7 @@ const smsEditorRef = ref<{
|
||||
validateFields?: () => ReturnType<NonNullable<FormInstance['validateFields']>>
|
||||
resetAll?: () => void
|
||||
}>()
|
||||
const sendStore = useGroupSmsSendStore()
|
||||
|
||||
const form = ref<Sms>({
|
||||
message: '',
|
||||
@@ -55,7 +66,17 @@ const form = ref<Sms>({
|
||||
|
||||
const addSMSVisible = ref<boolean>(false)
|
||||
|
||||
onMounted(() => getGroups())
|
||||
onMounted(() => {
|
||||
void initWalletSettingsFromApi()
|
||||
getGroups()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => sendStore.finished,
|
||||
(finished) => {
|
||||
if (finished) refreshTable()
|
||||
}
|
||||
)
|
||||
|
||||
async function getGroups() {
|
||||
try {
|
||||
@@ -73,10 +94,20 @@ function refreshTable() {
|
||||
}
|
||||
|
||||
function closeSmsSheet() {
|
||||
if (loading.value) return
|
||||
addSMSVisible.value = false
|
||||
}
|
||||
|
||||
async function add() {
|
||||
if (!groupSmsEnabledSetting.value) {
|
||||
message.warning('ارسال پیامک گروهی غیرفعال است')
|
||||
return
|
||||
}
|
||||
if (sendStore.active) {
|
||||
message.info('یک ارسال گروهی در حال انجام است')
|
||||
sendStore.expand()
|
||||
return
|
||||
}
|
||||
addSMSVisible.value = true
|
||||
resetData()
|
||||
}
|
||||
@@ -90,22 +121,39 @@ function resetData() {
|
||||
smsEditorRef.value?.resetAll?.()
|
||||
}
|
||||
|
||||
function uniqueNumbers(list: string[]): string[] {
|
||||
return Array.from(new Set(list.map((n) => String(n).trim()).filter(Boolean)))
|
||||
}
|
||||
|
||||
function ok() {
|
||||
if (sendStore.active) {
|
||||
message.warning('ارسال قبلی هنوز تمام نشده است')
|
||||
return
|
||||
}
|
||||
|
||||
smsEditorRef.value?.validateFields?.().then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
if (!form.value.numbers) form.value.numbers = []
|
||||
form.value.numbers.push(...(form.value.single ?? []))
|
||||
await createSMS(form.value)
|
||||
resetData()
|
||||
addSMSVisible.value = false
|
||||
refreshTable()
|
||||
message.success('با موفقیت ارسال شد')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
const numbers = uniqueNumbers([...(form.value.numbers ?? []), ...(form.value.single ?? [])])
|
||||
if (!numbers.length) {
|
||||
message.warning('حداقل یک شماره انتخاب کنید')
|
||||
return
|
||||
}
|
||||
if (!form.value.message?.trim()) {
|
||||
message.warning('متن پیامک خالی است')
|
||||
return
|
||||
}
|
||||
|
||||
const text = form.value.message
|
||||
addSMSVisible.value = false
|
||||
resetData()
|
||||
await sendStore.start(numbers, text)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sms-editor-scroll {
|
||||
max-height: min(62vh, 560px);
|
||||
overflow-y: auto;
|
||||
padding-inline-end: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { conditionalOrderEnabledSetting, priceTriggerEnabledSetting } from '@/utils/config'
|
||||
import {
|
||||
conditionalOrderEnabledSetting,
|
||||
priceProviderSetting,
|
||||
priceTriggerEnabledSetting
|
||||
} from '@/utils/config'
|
||||
|
||||
const HesabKetab: RouteRecordRaw = {
|
||||
name: 'hesabKetab',
|
||||
@@ -111,6 +115,21 @@ const HesabKetab: RouteRecordRaw = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'colleagueOrdersMain',
|
||||
path: 'colleague-orders',
|
||||
component: () => import('@/pages/menuPages/colleagueOrders/index.vue'),
|
||||
meta: {
|
||||
icon: 'vuesax-linear:people',
|
||||
title: 'تراکنش همکار',
|
||||
order: 6.8
|
||||
},
|
||||
beforeEnter() {
|
||||
if (priceProviderSetting.value !== 'montzar') {
|
||||
return { name: 'accountsMain' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'accountingMain',
|
||||
path: 'accounting',
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { chunk } from 'lodash'
|
||||
import { createSMS } from '@/api/sms'
|
||||
|
||||
export interface GroupSmsSendError {
|
||||
batch: number
|
||||
numbers: string[]
|
||||
error: string
|
||||
}
|
||||
|
||||
export type GroupSmsSendStatus = 'idle' | 'running' | 'done' | 'canceled'
|
||||
export type GroupSmsSendOutcome = 'running' | 'success' | 'error' | 'partial' | 'canceled'
|
||||
|
||||
/** هر درخواست حداکثر ۱۰ شماره — پنل پیامک زیر فشار نرود */
|
||||
const BATCH_SIZE = 10
|
||||
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
/**
|
||||
* ارسال گروهی پیامک — مثل ایمپورت مشتری حسابداری:
|
||||
* در پسزمینه ادامه مییابد، مینیمایز میشود، ۱۰تایی پیش میرود.
|
||||
*/
|
||||
export const useGroupSmsSendStore = defineStore('groupSmsSend', () => {
|
||||
const status = ref<GroupSmsSendStatus>('idle')
|
||||
const messageText = ref('')
|
||||
const queue = ref<string[]>([])
|
||||
const total = ref(0)
|
||||
const processed = ref(0)
|
||||
const success = ref(0)
|
||||
const failed = ref(0)
|
||||
const errors = ref<GroupSmsSendError[]>([])
|
||||
const currentBatch = ref(0)
|
||||
const totalBatches = ref(0)
|
||||
|
||||
const visible = ref(false)
|
||||
const minimized = ref(false)
|
||||
|
||||
let cancelRequested = false
|
||||
|
||||
const active = computed(() => status.value === 'running')
|
||||
const finished = computed(() => status.value === 'done' || status.value === 'canceled')
|
||||
const percent = computed(() =>
|
||||
total.value > 0 ? Math.min(100, Math.round((processed.value / total.value) * 100)) : 0,
|
||||
)
|
||||
const currentLabel = computed(() => {
|
||||
if (!active.value) return ''
|
||||
if (totalBatches.value <= 0) return '...'
|
||||
return `دسته ${Math.min(currentBatch.value + 1, totalBatches.value)} از ${totalBatches.value}`
|
||||
})
|
||||
|
||||
const outcome = computed<GroupSmsSendOutcome>(() => {
|
||||
if (status.value === 'running' || status.value === 'idle') return 'running'
|
||||
if (status.value === 'canceled') return 'canceled'
|
||||
if (failed.value === 0) return 'success'
|
||||
if (success.value === 0) return 'error'
|
||||
return 'partial'
|
||||
})
|
||||
|
||||
function reset() {
|
||||
status.value = 'idle'
|
||||
messageText.value = ''
|
||||
queue.value = []
|
||||
total.value = 0
|
||||
processed.value = 0
|
||||
success.value = 0
|
||||
failed.value = 0
|
||||
errors.value = []
|
||||
currentBatch.value = 0
|
||||
totalBatches.value = 0
|
||||
visible.value = false
|
||||
minimized.value = false
|
||||
cancelRequested = false
|
||||
}
|
||||
|
||||
function expand() {
|
||||
minimized.value = false
|
||||
}
|
||||
|
||||
function minimize() {
|
||||
minimized.value = true
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (status.value === 'running') cancelRequested = true
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
if (status.value === 'running') return
|
||||
reset()
|
||||
}
|
||||
|
||||
async function start(numbers: string[], message: string) {
|
||||
if (status.value === 'running') return
|
||||
|
||||
const unique = Array.from(new Set(numbers.map((n) => String(n).trim()).filter(Boolean)))
|
||||
if (!unique.length || !message.trim()) return
|
||||
|
||||
messageText.value = message
|
||||
queue.value = unique
|
||||
total.value = unique.length
|
||||
processed.value = 0
|
||||
success.value = 0
|
||||
failed.value = 0
|
||||
errors.value = []
|
||||
currentBatch.value = 0
|
||||
totalBatches.value = Math.ceil(unique.length / BATCH_SIZE)
|
||||
cancelRequested = false
|
||||
status.value = 'running'
|
||||
visible.value = true
|
||||
minimized.value = false
|
||||
|
||||
await runLoop()
|
||||
}
|
||||
|
||||
async function sendBatchWithRetry(batchNumbers: string[], attempt = 0): Promise<void> {
|
||||
try {
|
||||
await createSMS({
|
||||
message: messageText.value,
|
||||
numbers: batchNumbers
|
||||
})
|
||||
} catch (e) {
|
||||
if (attempt < 2 && !cancelRequested) {
|
||||
await sleep(1500 * (attempt + 1))
|
||||
return sendBatchWithRetry(batchNumbers, attempt + 1)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function runLoop() {
|
||||
const batches = chunk(queue.value, BATCH_SIZE)
|
||||
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
if (cancelRequested) {
|
||||
status.value = 'canceled'
|
||||
return
|
||||
}
|
||||
|
||||
currentBatch.value = i
|
||||
const batchNumbers = batches[i]
|
||||
|
||||
try {
|
||||
await sendBatchWithRetry(batchNumbers)
|
||||
success.value += batchNumbers.length
|
||||
} catch {
|
||||
failed.value += batchNumbers.length
|
||||
errors.value.push({
|
||||
batch: i + 1,
|
||||
numbers: batchNumbers,
|
||||
error: 'خطا در ارتباط با پنل پیامک'
|
||||
})
|
||||
}
|
||||
|
||||
processed.value += batchNumbers.length
|
||||
if (i < batches.length - 1) await sleep(250)
|
||||
}
|
||||
|
||||
status.value = 'done'
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
messageText,
|
||||
queue,
|
||||
total,
|
||||
processed,
|
||||
success,
|
||||
failed,
|
||||
errors,
|
||||
currentBatch,
|
||||
totalBatches,
|
||||
visible,
|
||||
minimized,
|
||||
active,
|
||||
finished,
|
||||
percent,
|
||||
currentLabel,
|
||||
outcome,
|
||||
start,
|
||||
cancel,
|
||||
dismiss,
|
||||
expand,
|
||||
minimize,
|
||||
reset
|
||||
}
|
||||
})
|
||||
@@ -55,6 +55,9 @@ export const customerLevelProfitEnabledSetting = ref(false)
|
||||
/** reactive — منو و UI هشدار قیمت */
|
||||
export const priceTriggerEnabledSetting = ref(false)
|
||||
|
||||
/** reactive — دکمه ارسال پیامک گروهی در صفحه پیامکها */
|
||||
export const groupSmsEnabledSetting = ref(false)
|
||||
|
||||
/** reactive — منو و زنگوله اعلان داخل اپ */
|
||||
export const appNotificationEnabledSetting = ref(true)
|
||||
|
||||
@@ -149,6 +152,10 @@ export function setCustomerDirectSmsEnabledFromSettings(value: boolean): void {
|
||||
customerDirectSmsEnabledSetting.value = value
|
||||
}
|
||||
|
||||
export function setGroupSmsEnabledFromSettings(value: boolean): void {
|
||||
groupSmsEnabledSetting.value = value
|
||||
}
|
||||
|
||||
export function setConditionalOrderEnabledFromSettings(value: boolean): void {
|
||||
conditionalOrderEnabledSetting.value = value
|
||||
}
|
||||
@@ -250,6 +257,11 @@ export function applyCustomerDirectSmsFromRows(rows: Array<{ key?: string; value
|
||||
if (row) setCustomerDirectSmsEnabledFromSettings(isSwitchOn(row.value))
|
||||
}
|
||||
|
||||
export function applyGroupSmsFromRows(rows: Array<{ key?: string; value?: string | null }>) {
|
||||
const row = rows.find((item) => item.key === 'group_sms')
|
||||
if (row) setGroupSmsEnabledFromSettings(isSwitchOn(row.value))
|
||||
}
|
||||
|
||||
export function applyConditionalOrderEnabledFromRows(
|
||||
rows: Array<{ key?: string; value?: string | null }>
|
||||
) {
|
||||
@@ -317,6 +329,7 @@ async function bootstrapSettingsFromApi(): Promise<void> {
|
||||
applyBannerEnabledFromRows(list)
|
||||
applyTrackingSystemsEnabledFromRows(list)
|
||||
applyCustomerDirectSmsFromRows(list)
|
||||
applyGroupSmsFromRows(list)
|
||||
applyConditionalOrderEnabledFromRows(list)
|
||||
applyProductFundsEnabledFromRows(list)
|
||||
applyCustomerLevelProfitEnabledFromRows(list)
|
||||
|
||||
Reference in New Issue
Block a user