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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fbe7b98ada
commit
fbe4f38e06
@@ -0,0 +1,81 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user