feat: complete admin panel and backend admin api migration

This commit is contained in:
s.esmaeili
2026-09-13 21:02:25 +03:30
parent 03119cd655
commit 267fc7b7d1
239 changed files with 25431 additions and 935 deletions
@@ -0,0 +1,61 @@
<template>
<!-- Ported from legacy henza_admin/src/views/notify/create.vue. -->
<a-spin :spinning="loading">
<a-card title="ارسال نوتیفیکیشن">
<a-form layout="vertical" @submit.prevent="handleSubmit">
<div class="grid-2">
<a-form-item label="عنوان" :validate-status="err.title ? 'error' : ''" :help="err.title">
<a-input v-model:value="form.title" />
</a-form-item>
<a-form-item label="نوع کاربر">
<a-select v-model:value="form.type">
<a-select-option :value="0">همه مشتریان</a-select-option>
<a-select-option :value="2">مشتریان عمده</a-select-option>
<a-select-option :value="1">کارشناسان فروش</a-select-option>
</a-select>
</a-form-item>
</div>
<a-form-item label="متن پیام" :validate-status="err.text ? 'error' : ''" :help="err.text">
<a-textarea v-model:value="form.text" :auto-size="{ minRows: 4, maxRows: 8 }" />
</a-form-item>
<a-button type="primary" @click="handleSubmit">ارسال</a-button>
</a-form>
</a-card>
</a-spin>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { message } from 'ant-design-vue';
import api from '../../api/client';
const loading = ref(false);
const form = reactive<Record<string, any>>({ title: '', text: '', type: 0 });
const err = reactive<Record<string, string>>({});
async function handleSubmit() {
Object.keys(err).forEach((k) => delete err[k]);
if (!form.title) err.title = 'عنوان الزامی است';
if (!form.text) err.text = 'متن پیام الزامی است';
if (Object.keys(err).length) return;
try {
loading.value = true;
const { data } = await api.post('notify', form);
message.success('نوتیفیکیشن ارسال شد');
form.title = '';
form.text = '';
} catch (e: any) {
message.error(e?.response?.data?.message ?? 'خطا در ارسال');
} finally {
loading.value = false;
}
}
</script>
<style scoped>
.grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }
</style>
@@ -0,0 +1,15 @@
// Ported from legacy henza_admin/src/views/notify/notifyTbl.js
import XTbl, { Xtc } from '../../components/x-table/model';
export function buildNotifyModel(): XTbl {
const tbl = new XTbl('admin/misc/notify/list', 'لیست نوتیفیکیشن های دریافتی');
tbl.add(new Xtc('id', 'شماره'));
tbl.add(new Xtc('title', 'عنوان'));
// legacy column key is 'note' but the payload carries the body as `text`
tbl.add(new Xtc('text', 'متن').noSort());
tbl.add(new Xtc('opt', 'عملیات').noSort().renderSlot('opt'));
tbl.noFilters();
return tbl;
}
export default buildNotifyModel;
@@ -0,0 +1,93 @@
<template>
<!--
Ported from legacy henza_admin/src/views/notify/user.vue — the received
notifications list, with a read-state modal.
-->
<a-card title="لیست نوتیفیکیشن های دریافتی">
<XTable ref="tbl" :model="model">
<template #opt="{ record }">
<a-tooltip title="مشاهده">
<EyeOutlined class="cursor-pointer mx-1 text-primary" @click="view(record)" />
</a-tooltip>
</template>
</XTable>
<a-modal v-model:open="visible" title="مشاهده نوتیفیکیشن" width="60%">
<div v-if="tempRow">
<a-descriptions bordered size="small" :column="1">
<a-descriptions-item label="عنوان">{{ tempRow.title }}</a-descriptions-item>
<a-descriptions-item label="متن">{{ tempRow.body }}</a-descriptions-item>
<a-descriptions-item label="ارسال کننده">{{ tempRow.sender ?? '-' }}</a-descriptions-item>
<a-descriptions-item label="خوانده شده">
{{ tempRow.read_count }} از {{ tempRow.total_count }}
</a-descriptions-item>
</a-descriptions>
<a-table
class="mt-4"
size="small"
:data-source="tempRow.recipients"
:columns="recipientColumns"
:pagination="{ pageSize: 10 }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'is_read'">
<a-tag v-if="record.is_read" color="green">خوانده شده</a-tag>
<a-button v-else size="small" @click="markRead(record)">علامت‌گذاری</a-button>
</template>
<template v-else-if="column.dataIndex === 'customer'">
{{ record.customer.name }}
</template>
</template>
</a-table>
</div>
<a-empty v-else />
<template #footer>
<a-button @click="visible = false">بستن</a-button>
</template>
</a-modal>
</a-card>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { message } from 'ant-design-vue';
import { EyeOutlined } from '@ant-design/icons-vue';
import XTable from '../../components/x-table/XTable.vue';
import { buildNotifyModel } from './notifyTbl';
import api from '../../api/client';
const model = buildNotifyModel();
const tbl = ref<{ fetch: () => Promise<void> } | null>(null);
const visible = ref(false);
const tempRow = ref<Record<string, any> | undefined>(undefined);
const recipientColumns = [
{ title: 'مشتری', dataIndex: 'customer' },
{ title: 'وضعیت', dataIndex: 'is_read' },
];
async function view(record: Record<string, any>) {
const { data } = await api.get(`notify/${record.id}`);
tempRow.value = data;
visible.value = true;
}
/** legacy GET notify/read/{id} */
async function markRead(recipient: Record<string, any>) {
try {
await api.get(`notify/read/${recipient.id}`);
recipient.is_read = true;
if (tempRow.value) tempRow.value.read_count += 1;
message.success('علامت‌گذاری شد');
} catch (e: any) {
message.error(e?.response?.data?.message ?? 'خطا');
}
}
</script>
<style scoped>
.mt-4 { margin-top: 1rem; }
</style>