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,228 @@
<template>
<!--
Ported from legacy henza_admin/src/views/offersIgnore/offersIgnore-add-edit.vue.
Pick an exclusion type, then tick the specific products / categories /
customers / brands to exclude from the automatic discount. The ticked set is
the complete selection for that type — legacy posts `{type, item_id: [...]}`
and the server replaces that type's rows with it.
The card title in legacy is "تعریف تخفیف" (the same string the promotion form
uses) even though this screen defines EXCLUSIONS — preserved, not corrected.
-->
<div id="page-user-view">
<div id="user-data">
<a-spin :spinning="loading">
<a-form layout="vertical">
<a-card title="تعریف تخفیف" class="mb-base mb-10">
<div class="grid-4">
<a-form-item label="نوع استثناء">
<a-select v-model:value="type" @change="changeTable">
<a-select-option v-for="(label, code) in types" :key="code" :value="Number(code)">
{{ label }}
</a-select-option>
</a-select>
</a-form-item>
</div>
<template v-if="type === 1">
<h6>تعداد {{ selectedRow.length }} محصول انتخاب شده است</h6>
<XTable ref="tblProducts" v-model="selectedRow" :model="productCols" :has-selection="true">
<template #image="{ record }">
<img
width="90px"
height="90px"
style="object-fit: scale-down"
:src="record.thumb && record.thumb.full ? record.thumb.full : noImage"
/>
</template>
</XTable>
<h6>تعداد {{ selectedRow.length }} محصول انتخاب شده است</h6>
</template>
<template v-if="type === 2">
<h6>تعداد {{ selectedRow.length }} دسته بندی انتخاب شده است</h6>
<XTable ref="tblCategory" v-model="selectedRow" :model="categoryCols" :has-selection="true" />
<h6>تعداد {{ selectedRow.length }} دسته بندی انتخاب شده است</h6>
</template>
<template v-if="type === 3">
<h6>تعداد {{ selectedRow.length }} مشتری انتخاب شده است</h6>
<XTable ref="tblCustomer" v-model="selectedRow" :model="customerCols" :has-selection="true" />
<h6>تعداد {{ selectedRow.length }} مشتری انتخاب شده است</h6>
</template>
<template v-if="type === 4">
<h6>تعداد {{ selectedRow.length }} برند انتخاب شده است</h6>
<XTable ref="tblBrand" v-model="selectedRow" :model="brandCols" :has-selection="true" />
<h6>تعداد {{ selectedRow.length }} برند انتخاب شده است</h6>
</template>
<div class="mt-5">
<a-button type="primary" class="mr-4" @click="handleSubmit">ثبت</a-button>
</div>
</a-card>
</a-form>
</a-spin>
<a-spin :spinning="deleteLoading">
<a-card title="حذف نوع استثناء">
<div class="grid-4 items-end">
<a-form-item label="نوع">
<a-select v-model:value="deleteType">
<a-select-option v-for="(label, code) in types" :key="code" :value="Number(code)">
{{ label }}
</a-select-option>
</a-select>
</a-form-item>
<a-popconfirm
title="آیا مطمئن هستید؟"
ok-text="بله"
cancel-text="خیر"
@confirm="deleteIgnoreType"
>
<a-button danger>حذف</a-button>
</a-popconfirm>
</div>
</a-card>
</a-spin>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { message } from 'ant-design-vue';
import XTable from '../../components/x-table/XTable.vue';
import XTbl, { Xtc } from '../../components/x-table/model';
import api from '../../api/client';
const noImage = '/img/no-items.png';
const loading = ref(false);
const deleteLoading = ref(false);
/** Legacy's exclusion types reuse the promotion scope codes. */
const types: Record<number, string> = {
1: 'محصولات',
2: 'دسته بندی',
3: 'مشتری',
4: 'برند',
};
const type = ref(1);
const deleteType = ref(1);
const selectedRow = ref<Record<string, any>[]>([]);
const ignoreList = ref<{ id: string; type: number; item_id: string }[]>([]);
// One picker table per type.
const productCols = buildProducts();
const categoryCols = buildCategories();
const customerCols = buildCustomers();
const brandCols = buildBrands();
function buildProducts(): XTbl {
const t = new XTbl('admin/products/list', 'محصولات');
t.add(new Xtc('id', 'کد').filterNumber());
t.add(new Xtc('ProductImage', 'تصویر').renderSlot('image').noSort());
t.add(new Xtc('title', 'نام محصول').filterText());
t.add(new Xtc('product_code', 'کد محصول').filterText());
return t;
}
function buildCategories(): XTbl {
const t = new XTbl('admin/categories/list', 'دسته بندی');
t.add(new Xtc('id', 'کد').filterNumber());
t.add(new Xtc('title', 'عنوان دسته').filterText());
t.add(new Xtc('type_str', 'نوع نمایش').noSort());
return t;
}
function buildCustomers(): XTbl {
const t = new XTbl('admin/customers-omde/list', 'مشتریان');
t.add(new Xtc('id', 'کد').filterNumber());
t.add(new Xtc('name', 'نام و نام خانوادگی').filterText());
t.add(new Xtc('mobile', 'موبایل').filterText().noSort());
return t;
}
function buildBrands(): XTbl {
const t = new XTbl('admin/products/brands-table', 'برندها');
t.add(new Xtc('id', 'کد').filterNumber());
t.add(new Xtc('title', 'عنوان برند').filterText());
return t;
}
/** legacy items_id(type): the already-excluded ids for a type */
function itemsId(t: number) {
return ignoreList.value.filter((i) => i.type === t).map((x) => x.item_id);
}
/** legacy changeTable(): reload the tick state when the type changes */
function changeTable(t: number) {
const ids = itemsId(t);
selectedRow.value = ids.map((id) => ({ id }));
}
async function handleSubmit() {
try {
loading.value = true;
await api.post('ignore', {
type: type.value,
item_id: selectedRow.value.map((r) => r.id),
});
message.success('عملیات با موفقیت انجام شد');
await getData();
changeTable(type.value);
} catch (e: any) {
console.log(e);
message.error(e?.response?.data?.message ?? 'خطا در ثبت اطلاعات.');
} finally {
loading.value = false;
}
}
async function deleteIgnoreType() {
try {
deleteLoading.value = true;
await api.delete(`ignore/${deleteType.value}`);
message.success('حذف شد');
await getData();
changeTable(type.value);
} catch (e: any) {
message.error(e?.response?.data?.message ?? 'خطا در حذف');
} finally {
deleteLoading.value = false;
}
}
async function getData() {
try {
loading.value = true;
const { data } = await api.get('ignore');
ignoreList.value = data;
// legacy seeds the selection from type 1 on load
selectedRow.value = itemsId(1).map((id) => ({ id }));
} catch (e) {
console.log(e);
message.error('خطا در دریافت اطلاعات');
} finally {
loading.value = false;
}
}
onMounted(getData);
</script>
<style scoped>
.grid-4 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.items-end {
align-items: end;
}
.mb-10 {
margin-bottom: 2.5rem;
}
.mt-5 {
margin-top: 1.25rem;
}
</style>