Cities (owner: as legacy) - City.parentId (self relation) replaces the flat `province` label; the migration turns each label into a parent row («استان …») first, so nothing is lost. - Admin pickers are legacy's tree (CityTreeSelect): the top level, the country, is a heading. The city form and grid get «والد». - Restored legacy _getP: a city without its own percent takes its ancestor's. Restored the omde-customer form's city → percent autofill. - Import copies legacy parents; delete refuses a city with children; a parent loop is refused. Permissions (owner approved the deletion) - dashboard / dashboard.omde / dashboard.tak rows become dashboard.view, then are deleted. - Import maps them the same way: legacy types 102, 104, 105, 108 and 135 had lost the dashboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
2.5 KiB
Vue
82 lines
2.5 KiB
Vue
<template>
|
|
<!--
|
|
Legacy's city picker (henza_admin mixins/citiesModel.js): the cities as a
|
|
tree — country > province > city > district — with the top level a heading
|
|
that cannot be picked. A top-level row with nothing under it stays pickable,
|
|
so a city entered without a parent is never unreachable.
|
|
`parentPicker` is the «والد» field of the city form: every row is pickable
|
|
there except the city being edited and its own descendants.
|
|
-->
|
|
<a-tree-select
|
|
:value="value ?? undefined"
|
|
:tree-data="treeData"
|
|
show-search
|
|
allow-clear
|
|
tree-default-expand-all
|
|
tree-node-filter-prop="title"
|
|
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
|
|
:placeholder="placeholder ?? 'شهر را انتخاب کنید'"
|
|
class="w-full"
|
|
@update:value="onChange"
|
|
/>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue';
|
|
|
|
export interface CityRow {
|
|
id: string;
|
|
title: string;
|
|
parent?: string | null;
|
|
}
|
|
|
|
interface Node {
|
|
value: string;
|
|
key: string;
|
|
title: string;
|
|
disabled?: boolean;
|
|
children?: Node[];
|
|
}
|
|
|
|
const props = defineProps<{
|
|
value?: string | null;
|
|
cities: CityRow[];
|
|
parentPicker?: boolean;
|
|
/** parentPicker: the city being edited — it and its subtree are not offered. */
|
|
excludeId?: string;
|
|
placeholder?: string;
|
|
}>();
|
|
const emit = defineEmits<{
|
|
(e: 'update:value', v: string | undefined): void;
|
|
(e: 'change', v: string | undefined): void;
|
|
}>();
|
|
|
|
const treeData = computed<Node[]>(() => {
|
|
const ids = new Set(props.cities.map((c) => c.id));
|
|
const kids = new Map<string | null, CityRow[]>();
|
|
for (const c of props.cities) {
|
|
const p = c.parent && ids.has(c.parent) && c.parent !== c.id ? c.parent : null;
|
|
if (!kids.has(p)) kids.set(p, []);
|
|
kids.get(p)!.push(c);
|
|
}
|
|
const seen = new Set<string>();
|
|
const build = (parent: string | null, depth: number): Node[] =>
|
|
(kids.get(parent) ?? [])
|
|
.filter((c) => !seen.has(c.id) && !(props.parentPicker && c.id === props.excludeId))
|
|
.map((c) => {
|
|
seen.add(c.id);
|
|
const children = build(c.id, depth + 1);
|
|
const node: Node = { value: c.id, key: c.id, title: c.title };
|
|
if (children.length) node.children = children;
|
|
if (!props.parentPicker && depth === 0 && children.length) node.disabled = true;
|
|
return node;
|
|
});
|
|
return build(null, 0);
|
|
});
|
|
|
|
function onChange(v: string | undefined) {
|
|
emit('update:value', v ?? undefined);
|
|
emit('change', v ?? undefined);
|
|
}
|
|
</script>
|