fix notif 3

This commit is contained in:
2026-07-24 23:12:36 +03:30
parent 3c41a3d23f
commit 5b11dccbb8
8 changed files with 742 additions and 132 deletions
+31 -1
View File
@@ -1,6 +1,12 @@
import type { TableInput } from '@core/type/baseTable'
import useAxios from '@core/utils/useAxios'
import type { AppNotification, AppNotificationReadersResponse } from '@/model/appNotifications'
import type {
AppNotification,
AppNotificationCustomerOption,
AppNotificationReadersResponse,
AppNotificationRecipientsResponse,
AppNotificationSegmentOption,
} from '@/model/appNotifications'
export const getAppNotificationsTable = async (params: TableInput) =>
await useAxios.post<Array<AppNotification>>('/admin/app_notifications/table', params)
@@ -20,3 +26,27 @@ export const deleteAppNotification = async (id: number) =>
export const getAppNotificationReaders = async (id: number) =>
await useAxios.get<AppNotificationReadersResponse>(`/admin/app_notifications/${id}/readers`)
export const getAppNotificationRecipients = async (id: number) =>
await useAxios.get<AppNotificationRecipientsResponse>(`/admin/app_notifications/${id}/recipients`)
export const getAppNotificationAudienceOptions = async () =>
await useAxios.get<{ segments: AppNotificationSegmentOption[] }>(
`/admin/app_notifications/audience-options`
)
export const searchAppNotificationCustomers = async (q: string) =>
await useAxios.get<{ customers: AppNotificationCustomerOption[] }>(
`/admin/app_notifications/search-customers`,
{ params: { q } }
)
export const previewAppNotificationAudience = async (payload: {
audience_type: string
segment_keys?: string[]
customer_ids?: number[]
}) =>
await useAxios.post<{
count: number | null
sample: AppNotificationCustomerOption[]
label: string
}>(`/admin/app_notifications/preview-audience`, payload)
+34
View File
@@ -1,3 +1,5 @@
export type AppNotificationAudienceType = 'all' | 'segment' | 'customers'
export interface AppNotification {
id?: number
title: string
@@ -8,6 +10,12 @@ export interface AppNotification {
created_at?: string
updated_at?: string
reads_count?: number
audience_type?: AppNotificationAudienceType
segment_keys?: string[] | null
audience_label?: string | null
recipients_count?: number
/** فقط هنگام ایجاد — لیست id مشتریان تکی */
customer_ids?: number[]
}
export const APP_NOTIFICATION_SEVERITIES = [
@@ -17,6 +25,24 @@ export const APP_NOTIFICATION_SEVERITIES = [
{ value: 'danger', label: 'مهم / خطر (danger)' },
] as const
export const APP_NOTIFICATION_AUDIENCE_TYPES = [
{ value: 'all', label: 'همه کاربران' },
{ value: 'segment', label: 'گروهی' },
{ value: 'customers', label: 'تکی / چندنفره' },
] as const
export interface AppNotificationSegmentOption {
key: string
name: string
description: string
}
export interface AppNotificationCustomerOption {
id: number
name?: string | null
mobile?: string | null
}
export interface AppNotificationReader {
customer_id: number
name: string
@@ -28,3 +54,11 @@ export interface AppNotificationReadersResponse {
reads_count: number
readers: AppNotificationReader[]
}
export interface AppNotificationRecipientsResponse {
audience_type: AppNotificationAudienceType
audience_label?: string | null
recipients_count: number | null
recipients: Array<{ customer_id: number; name: string; mobile?: string | null }>
note?: string
}
@@ -1,55 +1,541 @@
<template>
<a-form :model="form" ref="formRef" layout="vertical" autocomplete="off">
<a-form-item label="عنوان" name="title" :rules="inputRules()">
<BasicInput v-model:value="form.title" placeholder="عنوان اعلان" />
</a-form-item>
<a-form
:model="form"
ref="formRef"
layout="vertical"
autocomplete="off"
class="notif-editor"
>
<div class="notif-editor__grid">
<div class="notif-editor__main">
<a-form-item label="عنوان" name="title" :rules="inputRules()" class="notif-editor__item notif-editor__item--title">
<BasicInput v-model:value="form.title" placeholder="عنوان کوتاه اعلان" />
</a-form-item>
<a-form-item label="توضیح" name="body" :rules="inputRules()">
<a-textarea
v-model:value="form.body"
:rows="5"
placeholder="متن اعلان برای مشتری"
/>
</a-form-item>
<a-form-item
label="متن"
name="body"
:rules="inputRules()"
class="notif-editor__item notif-editor__item--body"
>
<a-textarea
v-model:value="form.body"
:rows="bodyRows"
:auto-size="bodyAutoSize"
placeholder="متن اعلان برای مشتری"
class="notif-editor__textarea"
/>
</a-form-item>
</div>
<a-form-item label="اهمیت" name="severity" :rules="inputRules()">
<BasicSelect
v-model:value="form.severity"
:items="severityItems"
key-name="value"
value-name="label"
placeholder="انتخاب اهمیت"
/>
</a-form-item>
<div class="notif-editor__side">
<div class="notif-editor__row">
<a-form-item
label="اهمیت"
name="severity"
:rules="inputRules()"
class="notif-editor__item notif-editor__item--grow"
>
<a-radio-group
v-model:value="form.severity"
option-type="button"
button-style="solid"
class="notif-editor__severity"
>
<a-radio-button
v-for="item in severityItems"
:key="item.value"
:value="item.value"
>
{{ item.short }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="وضعیت" name="is_active">
<a-switch
v-model:checked="form.is_active"
checked-children="فعال"
un-checked-children="غیرفعال"
/>
</a-form-item>
<a-form-item
label="وضعیت"
name="is_active"
class="notif-editor__item notif-editor__item--status"
>
<div class="notif-editor__status">
<a-switch v-model:checked="form.is_active" />
<span class="notif-editor__status-label">
{{ form.is_active ? 'فعال' : 'غیرفعال' }}
</span>
</div>
</a-form-item>
</div>
<template v-if="!isEdit">
<a-form-item
label="مخاطب"
name="audience_type"
:rules="inputRules()"
class="notif-editor__item notif-editor__item--audience"
>
<a-radio-group
v-model:value="form.audience_type"
option-type="button"
button-style="solid"
class="notif-editor__audience"
>
<a-radio-button
v-for="item in audienceTypes"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</a-radio-button>
</a-radio-group>
</a-form-item>
<div v-if="form.audience_type === 'segment'" class="notif-editor__segments">
<div class="notif-editor__section-label">انتخاب گروه</div>
<a-radio-group
:value="selectedSegment"
class="notif-editor__segment-grid"
@update:value="selectSegment"
>
<label
v-for="seg in segments"
:key="seg.key"
class="notif-editor__chip"
:class="{ 'notif-editor__chip--on': selectedSegment === seg.key }"
:title="seg.description"
>
<a-radio :value="seg.key" />
<span class="notif-editor__chip-text">{{ seg.name }}</span>
</label>
</a-radio-group>
</div>
<a-form-item
v-if="form.audience_type === 'customers'"
label="مشتریان"
class="notif-editor__item notif-editor__item--customers"
>
<a-select
v-model:value="form.customer_ids"
mode="multiple"
show-search
:filter-option="false"
:options="customerOptions"
:loading="customersLoading"
placeholder="جستجوی نام یا موبایل…"
class="w-full"
:max-tag-count="2"
@search="onCustomerSearch"
/>
</a-form-item>
<div class="notif-editor__preview">
<span v-if="previewLoading" class="notif-editor__preview-muted">در حال برآورد…</span>
<template v-else>
<span class="notif-editor__preview-dot" />
<span>
{{ previewLabel || 'مخاطب را انتخاب کنید' }}
<template v-if="previewCount != null">
· <b>{{ previewCount }}</b> نفر
</template>
<template v-else-if="form.audience_type === 'all'">
· همه
</template>
</span>
</template>
</div>
</template>
<a-alert
v-else
type="info"
show-icon
class="notif-editor__alert"
message="مخاطب بعد از ارسال ثابت است؛ فقط متن و وضعیت قابل ویرایش است."
/>
</div>
</div>
</a-form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import type { FormInstance } from 'ant-design-vue'
import type { AppNotification } from '@/model/appNotifications'
import { APP_NOTIFICATION_SEVERITIES } from '@/model/appNotifications'
import type {
AppNotification,
AppNotificationCustomerOption,
AppNotificationSegmentOption,
} from '@/model/appNotifications'
import { APP_NOTIFICATION_AUDIENCE_TYPES } from '@/model/appNotifications'
import {
getAppNotificationAudienceOptions,
previewAppNotificationAudience,
searchAppNotificationCustomers,
} from '@/api/appNotifications'
import { inputRules } from '@/utils/inputRules'
import BasicInput from '@/components/BasicInput.vue'
import BasicSelect from '@/components/BasicSelect.vue'
import { debounce } from 'lodash'
defineProps<{
const props = defineProps<{
form: Partial<AppNotification>
isEdit?: boolean
}>()
const severityItems = [...APP_NOTIFICATION_SEVERITIES]
const severityItems = [
{ value: 'info', short: 'اطلاعات' },
{ value: 'success', short: 'موفق' },
{ value: 'warning', short: 'هشدار' },
{ value: 'danger', short: 'مهم' },
] as const
const audienceTypes = [...APP_NOTIFICATION_AUDIENCE_TYPES]
const formRef = ref<FormInstance>()
const segments = ref<AppNotificationSegmentOption[]>([])
const customersLoading = ref(false)
const customerOptions = ref<Array<{ value: number; label: string }>>([])
const previewLoading = ref(false)
const previewCount = ref<number | null>(null)
const previewLabel = ref('')
const isNarrow = ref(false)
const isEdit = computed(() => Boolean(props.isEdit))
const selectedSegment = computed(() => props.form.segment_keys?.[0] || undefined)
const bodyRows = computed(() => (isNarrow.value ? 3 : 8))
const bodyAutoSize = computed(() =>
isNarrow.value
? { minRows: 3, maxRows: 5 }
: { minRows: 8, maxRows: 14 }
)
function selectSegment(key: string) {
props.form.segment_keys = key ? [key] : []
}
function syncNarrow() {
isNarrow.value = typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches
}
watch(
() => props.form.audience_type,
(type) => {
if (type !== 'segment') return
const keys = props.form.segment_keys || []
if (keys.length > 1) {
props.form.segment_keys = [keys[0]]
}
}
)
async function loadSegments() {
try {
const data = await getAppNotificationAudienceOptions()
segments.value = Array.isArray(data?.segments) ? data.segments : []
} catch (e) {
console.error(e)
}
}
const onCustomerSearch = debounce(async (q: string) => {
customersLoading.value = true
try {
const data = await searchAppNotificationCustomers(q || '')
const rows = Array.isArray(data?.customers) ? data.customers : []
customerOptions.value = rows.map((c: AppNotificationCustomerOption) => ({
value: c.id,
label: `${c.name || 'بدون نام'} — ${c.mobile || c.id}`,
}))
} catch (e) {
console.error(e)
} finally {
customersLoading.value = false
}
}, 300)
async function refreshPreview() {
if (isEdit.value) return
if (props.form.audience_type === 'all') {
previewCount.value = null
previewLabel.value = 'همه کاربران'
return
}
previewLoading.value = true
try {
const data = await previewAppNotificationAudience({
audience_type: String(props.form.audience_type || 'all'),
segment_keys: props.form.segment_keys || [],
customer_ids: props.form.customer_ids || [],
})
previewCount.value = data?.count ?? null
previewLabel.value = data?.label || ''
} catch (e) {
console.error(e)
} finally {
previewLoading.value = false
}
}
watch(
() => [
props.form.audience_type,
...(props.form.segment_keys || []),
...(props.form.customer_ids || []),
],
() => {
void refreshPreview()
}
)
onMounted(() => {
syncNarrow()
window.addEventListener('resize', syncNarrow)
void loadSegments()
void onCustomerSearch('')
void refreshPreview()
})
onUnmounted(() => {
window.removeEventListener('resize', syncNarrow)
})
defineExpose({
validateFields: () => formRef.value?.validateFields(),
resetFields: () => formRef.value?.resetFields(),
})
</script>
<style scoped>
.notif-editor__grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.25rem; /* حداقل gap-5 */
}
/* دسکتاپ: راست عنوان/متن (فضای بیشتر)، چپ بقیه */
@media (min-width: 768px) {
.notif-editor__grid {
grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
align-items: start;
gap: 1.25rem;
}
.notif-editor__main {
min-height: 100%;
}
.notif-editor__item--body {
flex: 1;
}
.notif-editor__textarea {
min-height: 12rem;
}
.notif-editor__segment-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/* موبایل: تک‌ستونه و مینیمال */
@media (max-width: 767px) {
.notif-editor__grid {
gap: 0.75rem;
}
.notif-editor :deep(.ant-form-item) {
margin-bottom: 0.55rem;
}
.notif-editor__segment-grid {
grid-template-columns: 1fr !important;
}
.notif-editor__severity :deep(.ant-radio-button-wrapper),
.notif-editor__audience :deep(.ant-radio-button-wrapper) {
font-size: 0.75rem;
padding-inline: 0.35rem;
}
.notif-editor__chip {
min-height: 2rem;
padding: 0.35rem 0.45rem;
}
.notif-editor__chip-text {
font-size: 0.7rem;
}
}
.notif-editor :deep(.ant-form-item) {
margin-bottom: 0.75rem;
}
.notif-editor__side {
display: flex;
flex-direction: column;
}
.notif-editor__side > .notif-editor__row {
margin-bottom: 0;
}
.notif-editor__item--audience {
margin-top: 1.25rem !important;
}
.notif-editor :deep(.ant-form-item-label) {
padding-bottom: 0.2rem;
}
.notif-editor :deep(.ant-form-item-label > label) {
font-size: 0.8125rem;
height: auto;
}
.notif-editor__item--grow {
flex: 1 1 auto;
min-width: 0;
}
.notif-editor__item--status {
flex: 0 0 auto;
}
.notif-editor__row {
display: flex;
align-items: flex-end;
gap: 0.75rem;
flex-wrap: wrap;
}
.notif-editor__severity,
.notif-editor__audience {
display: flex;
flex-wrap: wrap;
width: 100%;
}
.notif-editor__severity :deep(.ant-radio-button-wrapper),
.notif-editor__audience :deep(.ant-radio-button-wrapper) {
flex: 1 1 auto;
text-align: center;
height: 2rem;
line-height: 1.875rem;
font-size: 0.8125rem;
padding-inline: 0.5rem;
}
.notif-editor__status {
display: flex;
align-items: center;
gap: 0.5rem;
min-height: 2rem;
}
.notif-editor__status-label {
font-size: 0.8125rem;
opacity: 0.75;
}
.notif-editor__section-label {
font-size: 0.8125rem;
margin-bottom: 0.4rem;
opacity: 0.85;
}
.notif-editor__item--title {
margin-bottom: 0 !important;
}
.notif-editor__item--body {
margin-top: 1.25rem !important;
}
.notif-editor__segments {
margin-top: 1.25rem;
margin-bottom: 0.75rem;
}
.notif-editor__item--customers {
margin-top: 1.25rem !important;
}
.notif-editor__segment-grid {
display: grid !important;
grid-template-columns: 1fr;
gap: 0.35rem !important;
row-gap: 0.35rem !important;
column-gap: 0.35rem !important;
width: 100%;
}
.notif-editor__segment-grid :deep(.ant-radio-wrapper),
.notif-editor__segment-grid > .notif-editor__chip {
margin-inline-end: 0 !important;
}
.notif-editor__chip {
display: flex;
align-items: center;
gap: 0.35rem;
margin: 0;
padding: 0.4rem 0.5rem;
border-radius: 0.55rem;
border: 1px solid rgba(148, 163, 184, 0.35);
background: transparent;
color: inherit;
cursor: pointer;
text-align: right;
transition: border-color 0.15s ease, background 0.15s ease;
min-height: 2.15rem;
font: inherit;
}
.notif-editor__chip:hover {
border-color: rgba(37, 99, 235, 0.45);
}
.notif-editor__chip--on {
border-color: rgba(37, 99, 235, 0.55);
background: rgba(37, 99, 235, 0.1);
}
.notif-editor__chip :deep(.ant-radio-wrapper) {
margin: 0;
line-height: 1;
}
.notif-editor__chip-text {
font-size: 0.75rem;
font-weight: 600;
line-height: 1.3;
}
.notif-editor__preview {
display: flex;
align-items: center;
gap: 0.45rem;
margin-bottom: 0.25rem;
padding: 0.45rem 0.7rem;
border-radius: 999px;
font-size: 0.75rem;
background: rgba(148, 163, 184, 0.12);
width: fit-content;
max-width: 100%;
}
.notif-editor__preview-dot {
width: 0.4rem;
height: 0.4rem;
border-radius: 999px;
background: #2563eb;
flex-shrink: 0;
}
.notif-editor__preview-muted {
opacity: 0.65;
}
.notif-editor__alert {
margin-bottom: 0.25rem;
}
</style>
@@ -4,7 +4,7 @@
<template #extra>
<div class="flex items-center gap-2">
<AdminFieldHelp
text="اعلان‌های ثبت‌شده برای همهٔ مشتریان فعال در اپ نمایش داده می‌شود. فقط وقتی سوییچ «اعلان داخل اپ» روشن باشد."
text="اعلان را برای همه، گروه (مثلاً کسانی که مدتی نیامده‌اند) یا مشتری تکی بفرستید. کاربران آنلاین از طریق اتصال زنده اپ، بلافاصله بج اعلان را می‌بینند."
/>
<BaseButton type="primary" size="small" @click="emits('add')">
<span>افزودن اعلان</span>
@@ -16,6 +16,15 @@
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
<template #action="{ record }">
<div class="flex justify-center gap-2">
<a-tooltip placement="bottom">
<template #title>گیرندگان</template>
<Icon
icon="vuesax-linear:profile-2user"
color="#7C3AED"
class="cursor-pointer"
@click="emits('recipients', record)"
/>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>بازدیدکنندگان</template>
<Icon
@@ -75,6 +84,7 @@ const emits = defineEmits<{
toggle: [record: Record<string, unknown>]
delete: [id: number]
readers: [record: Record<string, unknown>]
recipients: [record: Record<string, unknown>]
}>()
const tblProps = {
@@ -16,6 +16,18 @@ const severityColor: Record<string, string> = {
danger: 'red',
}
const audienceTypeLabel: Record<string, string> = {
all: 'همه',
segment: 'گروهی',
customers: 'تکی',
}
const audienceTypeColor: Record<string, string> = {
all: 'purple',
segment: 'cyan',
customers: 'gold',
}
export const cols: BaseTableColumn[] = [
{
title: 'ردیف',
@@ -29,6 +41,29 @@ export const cols: BaseTableColumn[] = [
key: 'title',
type: 'Text',
},
{
title: 'نوع ارسال',
dataIndex: 'audience_type',
key: 'audience_type',
customRender: ({ record }) => (
<CustomTag color={audienceTypeColor[record.audience_type] || 'default'}>
{audienceTypeLabel[record.audience_type] || record.audience_type || 'همه'}
</CustomTag>
),
},
{
title: 'مخاطب',
dataIndex: 'audience_label',
key: 'audience_label',
customRender: ({ record }) => {
const label = record.audience_label || 'همه کاربران'
const count =
record.audience_type && record.audience_type !== 'all'
? ` (${Number(record.recipients_count ?? 0)} نفر)`
: ''
return `${label}${count}`
},
},
{
title: 'اهمیت',
dataIndex: 'severity',
@@ -68,6 +103,6 @@ export const cols: BaseTableColumn[] = [
slot: 'action',
fixed: 'right',
filterable: false,
width: 160,
width: 200,
},
]
+110 -10
View File
@@ -8,12 +8,13 @@
@toggle="toggleActive"
@delete="deleteItem"
@readers="openReaders"
@recipients="openRecipients"
/>
<BasicActionSheet
v-model:visible="editorVisible"
:title="sheetTitle"
modal-width="32rem"
modal-width="48rem"
drawer-height="85vh"
:loading="loading"
:show-button="true"
@@ -26,7 +27,11 @@
@cancel="closeEditor"
>
<div v-if="isMobile" class="mb-3 text-base font-bold">{{ sheetTitle }}</div>
<AppNotificationEditorFormBody ref="editorRef" :form="form" />
<AppNotificationEditorFormBody
ref="editorRef"
:form="form"
:is-edit="Boolean(editId)"
/>
</BasicActionSheet>
<BasicActionSheet
@@ -65,6 +70,43 @@
</div>
</a-spin>
</BasicActionSheet>
<BasicActionSheet
v-model:visible="recipientsVisible"
:title="recipientsTitle"
modal-width="28rem"
drawer-height="70vh"
:loading="recipientsLoading"
:show-button="true"
:show-cancel="false"
:show-ok="true"
ok-text="بستن"
@ok="recipientsVisible = false"
@close="recipientsVisible = false"
>
<div v-if="isMobile" class="mb-3 text-base font-bold">{{ recipientsTitle }}</div>
<a-spin :spinning="recipientsLoading">
<div v-if="recipientsNote" class="mb-3 text-sm text-gray-600">{{ recipientsNote }}</div>
<div v-if="!recipients.length && !recipientsNote" class="py-8 text-center text-gray-500">
گیرنده‌ای ثبت نشده است.
</div>
<div v-else-if="recipients.length" class="readers-scroll flex flex-col gap-2">
<div class="mb-1 text-xs text-gray-500">
{{ recipientsCountLabel }}
</div>
<div
v-for="row in recipients"
:key="row.customer_id"
class="flex items-center justify-between gap-3 rounded-lg border border-solid border-gray-100 px-3 py-2"
>
<div class="min-w-0">
<div class="truncate font-bold">{{ row.name }}</div>
<div class="text-xs text-gray-500" dir="ltr">{{ row.mobile || '—' }}</div>
</div>
</div>
</div>
</a-spin>
</BasicActionSheet>
</div>
</template>
@@ -80,6 +122,7 @@ import {
createAppNotification,
deleteAppNotification,
getAppNotificationReaders,
getAppNotificationRecipients,
toggleAppNotificationActive,
updateAppNotification,
} from '@/api/appNotifications'
@@ -102,11 +145,21 @@ const readersLoading = ref(false)
const readers = ref<AppNotificationReader[]>([])
const readersNotifTitle = ref('')
const recipientsVisible = ref(false)
const recipientsLoading = ref(false)
const recipients = ref<Array<{ customer_id: number; name: string; mobile?: string | null }>>([])
const recipientsNotifTitle = ref('')
const recipientsNote = ref('')
const recipientsTotal = ref<number | null>(null)
const defaultForm = (): Partial<AppNotification> => ({
title: '',
body: '',
severity: 'info',
is_active: true,
audience_type: 'all',
segment_keys: [],
customer_ids: [],
})
const form = ref<Partial<AppNotification>>(defaultForm())
@@ -115,6 +168,13 @@ const sheetTitle = computed(() => (editId.value ? 'ویرایش اعلان' : '
const readersTitle = computed(() =>
readersNotifTitle.value ? `بازدیدکنندگان — ${readersNotifTitle.value}` : 'بازدیدکنندگان اعلان'
)
const recipientsTitle = computed(() =>
recipientsNotifTitle.value ? `گیرندگان — ${recipientsNotifTitle.value}` : 'گیرندگان اعلان'
)
const recipientsCountLabel = computed(() => {
if (recipientsTotal.value == null) return `${recipients.value.length} نفر`
return `${recipientsTotal.value} نفر`
})
function refreshTable() {
tableRef.value?.fetch()
@@ -160,22 +220,62 @@ async function openReaders(record: AppNotification) {
}
}
async function openRecipients(record: AppNotification) {
if (!record.id) return
recipientsNotifTitle.value = record.title || ''
recipientsVisible.value = true
recipientsLoading.value = true
recipients.value = []
recipientsNote.value = ''
recipientsTotal.value = null
try {
const data = await getAppNotificationRecipients(record.id)
recipients.value = Array.isArray(data?.recipients) ? data.recipients : []
recipientsNote.value = data?.note || data?.audience_label || ''
recipientsTotal.value = data?.recipients_count ?? null
} catch (e) {
console.error(e)
message.error('بارگذاری لیست گیرندگان ناموفق بود')
} finally {
recipientsLoading.value = false
}
}
async function submitForm() {
try {
await editorRef.value?.validateFields?.()
loading.value = true
const payload = {
title: String(form.value.title ?? '').trim(),
body: String(form.value.body ?? '').trim(),
severity: form.value.severity || 'info',
is_active: Boolean(form.value.is_active),
if (!editId.value) {
const type = form.value.audience_type || 'all'
if (type === 'segment' && !(form.value.segment_keys?.length)) {
message.warning('یک گروه انتخاب کنید')
return
}
if (type === 'customers' && !(form.value.customer_ids?.length)) {
message.warning('حداقل یک مشتری انتخاب کنید')
return
}
}
loading.value = true
if (editId.value) {
await updateAppNotification(editId.value, payload)
await updateAppNotification(editId.value, {
title: String(form.value.title ?? '').trim(),
body: String(form.value.body ?? '').trim(),
severity: form.value.severity || 'info',
is_active: Boolean(form.value.is_active),
})
} else {
await createAppNotification(payload)
await createAppNotification({
title: String(form.value.title ?? '').trim(),
body: String(form.value.body ?? '').trim(),
severity: form.value.severity || 'info',
is_active: Boolean(form.value.is_active),
audience_type: form.value.audience_type || 'all',
segment_keys: form.value.segment_keys || [],
customer_ids: form.value.customer_ids || [],
})
}
message.success('اعلان با موفقیت ذخیره شد')