diff --git a/apps/admin-panel/src/components/common/CityTreeSelect.vue b/apps/admin-panel/src/components/common/CityTreeSelect.vue
new file mode 100644
index 0000000..0792021
--- /dev/null
+++ b/apps/admin-panel/src/components/common/CityTreeSelect.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
diff --git a/apps/admin-panel/src/views/apps/customer/ReagentCustomer.vue b/apps/admin-panel/src/views/apps/customer/ReagentCustomer.vue
index 2f5b225..f5df4d6 100644
--- a/apps/admin-panel/src/views/apps/customer/ReagentCustomer.vue
+++ b/apps/admin-panel/src/views/apps/customer/ReagentCustomer.vue
@@ -72,13 +72,10 @@
«شهر» also drives the commission autofill (ReagentCustomer.vue:111,215-228).
-->
-
@@ -117,6 +114,7 @@ import { onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
import api from '../../../api/client';
+import CityTreeSelect from '../../../components/common/CityTreeSelect.vue';
import ImageUploadInput from '../../../components/upload/ImageUploadInput.vue';
const route = useRoute();
@@ -147,7 +145,7 @@ const form = reactive>({
});
/** Cities for the «شهر» select and the commission autofill. */
-const cities = ref>([]);
+const cities = ref>([]);
const err = reactive>({});
/** Snapshot of the server's record, captured after the edit form is populated. */
@@ -180,7 +178,7 @@ function onAvatar(file: Record) {
* `if (!percent)`, so a city percent of 0 also falls through to the aux
* lookup. Replicated rather than "fixed" (D4).
*/
-async function onCityChange(value: string) {
+async function onCityChange(value: string | undefined) {
form.city_id = value;
if (!value) return;
@@ -246,6 +244,7 @@ onMounted(async () => {
cities.value = (data.rows ?? []).map((c: any) => ({
id: c.id,
title: c.title ?? c.name,
+ parent: c.parent ?? null,
percent: c.percent === null || c.percent === undefined ? null : Number(c.percent),
}));
} catch (e) {
diff --git a/apps/admin-panel/src/views/apps/customer/bulkCustomer.vue b/apps/admin-panel/src/views/apps/customer/bulkCustomer.vue
index 01eb674..dbe7124 100644
--- a/apps/admin-panel/src/views/apps/customer/bulkCustomer.vue
+++ b/apps/admin-panel/src/views/apps/customer/bulkCustomer.vue
@@ -98,17 +98,7 @@
-
-
- {{ c.title }}
-
-
+
@@ -211,17 +201,7 @@
-
-
- {{ c.title }}
-
-
+
@@ -269,6 +249,7 @@ import { message } from 'ant-design-vue';
import BaseFormModal from '../../../components/common/BaseFormModal.vue';
import ImageUploadInput from '../../../components/upload/ImageUploadInput.vue';
import api from '../../../api/client';
+import CityTreeSelect from '../../../components/common/CityTreeSelect.vue';
const props = defineProps<{
open?: boolean;
@@ -299,7 +280,7 @@ const router = useRouter();
const id = asModal ? props.id : (route.params.id as string | undefined);
const loading = ref(false);
-const cities = ref<{ id: string; title: string }[]>([]);
+const cities = ref<{ id: string; title: string; parent?: string | null; percent?: unknown }[]>([]);
const reagent = ref<{ id: string; name: string }[]>([]);
const form = reactive>({
@@ -369,8 +350,24 @@ function diffFromInitial(): Record {
return out;
}
-function filterCity(input: string, option: any) {
- return String(option?.children?.[0]?.children ?? '').includes(input);
+/**
+ * Legacy getPercent (henza_admin bulkCustomer.vue:260-272): picking a city fills
+ * «درصد» with the city's own percent, else its commission percent — which, as
+ * legacy's _getP, a city without one takes from its parent.
+ */
+async function onCityChange(value: string | undefined) {
+ if (!value) return;
+ let percent: unknown = cities.value.find((c) => c.id === value)?.percent;
+ if (!Number(percent)) {
+ try {
+ const { data } = await api.get(`cities/get-percent-aux/${value}`);
+ percent = data;
+ } catch (e) {
+ console.log(e);
+ return;
+ }
+ }
+ form.percent = percent === null || percent === undefined ? undefined : Number(percent);
}
function validate(): boolean {
diff --git a/apps/admin-panel/src/views/apps/customer/singleCustomer.vue b/apps/admin-panel/src/views/apps/customer/singleCustomer.vue
index 2c75f4c..c298e60 100644
--- a/apps/admin-panel/src/views/apps/customer/singleCustomer.vue
+++ b/apps/admin-panel/src/views/apps/customer/singleCustomer.vue
@@ -32,17 +32,7 @@
-
-
- {{ c.title }}
-
-
+
@@ -57,13 +47,14 @@ import { onMounted, reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
import api from '../../../api/client';
+import CityTreeSelect from '../../../components/common/CityTreeSelect.vue';
const route = useRoute();
const router = useRouter();
const id = route.params.id as string | undefined;
const loading = ref(false);
-const cities = ref<{ id: string; title: string }[]>([]);
+const cities = ref<{ id: string; title: string; parent?: string | null }[]>([]);
const form = reactive>({
fname: '',
@@ -74,10 +65,6 @@ const form = reactive>({
const err = reactive>({});
const REQUIRED = 'لطفا مقدار را وارد کنید';
-function filterOption(input: string, option: any) {
- return String(option?.children?.[0]?.children ?? '').includes(input);
-}
-
async function handleSubmit() {
Object.keys(err).forEach((k) => delete err[k]);
if (!form.fname) err.fname = REQUIRED;
diff --git a/apps/admin-panel/src/views/city/CityListModel.ts b/apps/admin-panel/src/views/city/CityListModel.ts
index e8ed860..eb6c22b 100644
--- a/apps/admin-panel/src/views/city/CityListModel.ts
+++ b/apps/admin-panel/src/views/city/CityListModel.ts
@@ -1,13 +1,13 @@
// Ported from legacy henza_admin/src/views/city/CityListModel.js
//
-// Note the 'level' column: legacy declares it but the cities payload has no such
-// field, so the cell always rendered blank. Preserved — see the port plan.
+// Legacy's 'level' column always rendered blank (the payload had no such
+// field); it is the parent city here, now that cities are a tree (PERS-02).
import XTbl, { Xtc } from '../../components/x-table/model';
export function buildCityListModel(): XTbl {
const tbl = new XTbl('admin/misc/cities/list', 'لیست شهر ها');
tbl.add(new Xtc('title', 'نام شهر').filterKey('name').filterText());
- tbl.add(new Xtc('level', 'سطح').filterNumber().noSort());
+ tbl.add(new Xtc('parent_title', 'والد').renderSlot('parent_title').noSort());
tbl.add(new Xtc('percent', 'درصد مشتری عمده').renderSlot('percent').noSort());
tbl.add(new Xtc('percent_aux', 'درصد کارشناس فروش').renderSlot('percent_aux').noSort());
tbl.add(new Xtc('opt', 'عملیات').noSort().renderSlot('opt'));
diff --git a/apps/admin-panel/src/views/city/cityList.vue b/apps/admin-panel/src/views/city/cityList.vue
index 193e525..31c10e1 100644
--- a/apps/admin-panel/src/views/city/cityList.vue
+++ b/apps/admin-panel/src/views/city/cityList.vue
@@ -1,8 +1,9 @@
@@ -11,6 +12,10 @@
+
+ {{ text ?? '—' }}
+
+
{{ text ?? '-' }}
@@ -43,6 +48,15 @@
+
+
+
@@ -68,6 +82,7 @@ import { buildCityListModel } from './CityListModel';
import { useUsersStore } from '../../stores/users';
import { useDeleteHandler } from '../../composables/useDeleteHandler';
import api from '../../api/client';
+import CityTreeSelect from '../../components/common/CityTreeSelect.vue';
const users = useUsersStore();
const hasPermission = (rid: string) => Boolean(users.hasPermission(rid));
@@ -77,26 +92,20 @@ const citiesTable = ref<{ fetch: () => Promise } | null>(null);
const addOpen = ref(false);
const visible = ref(false);
const tempRow = ref | undefined>(undefined);
-const form = reactive>({ title: '', percent: null, percent_aux: null });
+const form = reactive>({ title: '', parent: undefined, percent: null, percent_aux: null });
+const cities = ref<{ id: string; title: string; parent?: string | null }[]>([]);
-/**
- * legacy edit(): fetches the two percents separately before opening the modal.
- * Both are already on the row here, but the legacy calls are kept so the two
- * endpoints stay exercised and the values are always current.
- */
async function edit(row: Record) {
tempRow.value = row;
form.title = row.title ?? '';
+ form.parent = row.parent ?? undefined;
+ form.percent = row.percent === null || row.percent === undefined ? null : Number(row.percent);
+ form.percent_aux = row.percent_aux === null || row.percent_aux === undefined ? null : Number(row.percent_aux);
try {
- const [p, pa] = await Promise.all([
- api.get(`cities/get-percent/${row.id}`),
- api.get(`cities/get-percent-aux/${row.id}`),
- ]);
- form.percent = p.data === null ? null : Number(p.data);
- form.percent_aux = pa.data === null ? null : Number(pa.data);
+ const { data } = await api.post('cities/list');
+ cities.value = data.rows ?? [];
} catch (e) {
- form.percent = row.percent === null ? null : Number(row.percent);
- form.percent_aux = row.percent_aux === null ? null : Number(row.percent_aux);
+ console.log(e);
}
visible.value = true;
}
@@ -105,6 +114,7 @@ async function save() {
try {
await api.put(`cities/${tempRow.value?.id}`, {
title: form.title,
+ parent: form.parent ?? null,
percent: form.percent,
percent_aux: form.percent_aux,
});
diff --git a/apps/admin-panel/src/views/city/createCity.vue b/apps/admin-panel/src/views/city/createCity.vue
index 3aa6efa..3e5ba72 100644
--- a/apps/admin-panel/src/views/city/createCity.vue
+++ b/apps/admin-panel/src/views/city/createCity.vue
@@ -22,8 +22,14 @@
-
-
+
+
+
+
+
+
+
+
@@ -36,8 +42,14 @@
-
-
+
+
+
+
+
+
+
+
ثبت اطلاعات
@@ -47,11 +59,12 @@