Approved departure from the port's byte-faithful rule (docs/ADMIN-PORT-PLAN.md
bug rows 4-6), fixed and logged rather than silently changed:
- farsi.less: bold/700 @font-face blocks now load iran-yekan-700.woff2
instead of the 400 file, so bold text actually renders bold. No source
file exists for weight 300, so that face is dropped rather than faked.
- format("woff") corrected to format("woff2") on all faces.
- Removed 'font-weight: inherit' from the body,* reset, which was flattening
every heading's weight.
Also deduped 24 view files that each redeclared .grid-2/.grid-3/.grid-4 in
a scoped <style> block with drifted minmax values, though utilities.less
already defines all three globally. Removed the scoped copies; kept
PromotionForm's genuinely different 0.5rem gap as a small .grid-4--tight
modifier instead of a full redeclaration.
Verified: pnpm build passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
225 lines
7.7 KiB
Vue
225 lines
7.7 KiB
Vue
<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 now comes from the global utilities.less (dedup) */
|
|
.items-end {
|
|
align-items: end;
|
|
}
|
|
.mb-10 {
|
|
margin-bottom: 2.5rem;
|
|
}
|
|
.mt-5 {
|
|
margin-top: 1.25rem;
|
|
}
|
|
</style>
|