Files
hedena_gold/apps/admin-panel/src/components/common/CityTreeSelect.vue
T
s.esmaeiliandClaude Opus 5.5 fbe4f38e06 feat(cities): cities are a tree again, as legacy's; old dashboard permissions merged (PERS-02, PERM-01)
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>
2026-10-08 16:31:12 +03:30

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>