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,14 @@
|
||||
-- PERM-01 (owner approval 2026-10-08): legacy's two dashboard permissions
|
||||
-- «آمار عمده فروشی» (dashboard.omde) and «آمار تک فروشی» (dashboard.tak), and
|
||||
-- their tree parent "dashboard", became the single "dashboard.view" when the two
|
||||
-- dashboards became one. A database seeded before that change still holds the
|
||||
-- old rows: nothing reads them and the permission screen cannot show or revoke
|
||||
-- them. Whoever held one keeps the dashboard through "dashboard.view", then the
|
||||
-- old rows go. Data only, idempotent.
|
||||
INSERT INTO "permission" ("rid", "userTypeId")
|
||||
SELECT DISTINCT 'dashboard.view', "userTypeId"
|
||||
FROM "permission"
|
||||
WHERE "rid" IN ('dashboard', 'dashboard.omde', 'dashboard.tak')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
DELETE FROM "permission" WHERE "rid" IN ('dashboard', 'dashboard.omde', 'dashboard.tak');
|
||||
@@ -0,0 +1,33 @@
|
||||
-- PERS-02 (owner approval 2026-10-08): cities become a tree again, as in legacy
|
||||
-- (country > province > city > district, henza `cities.parent`). The flat
|
||||
-- `province` label is turned into parent rows, nothing is lost, then dropped.
|
||||
|
||||
ALTER TABLE "City" ADD COLUMN "parentId" TEXT;
|
||||
|
||||
-- One province row per distinct label, named as legacy names them
|
||||
-- («استان تهران»), unless a city row already carries that name.
|
||||
INSERT INTO "City" ("id", "name")
|
||||
SELECT 'cprov' || substr(md5(p.pname), 1, 20), p.pname
|
||||
FROM (
|
||||
SELECT DISTINCT
|
||||
CASE WHEN btrim("province") LIKE 'استان%' THEN btrim("province") ELSE 'استان ' || btrim("province") END AS pname
|
||||
FROM "City"
|
||||
WHERE "province" IS NOT NULL AND btrim("province") <> ''
|
||||
) p
|
||||
WHERE NOT EXISTS (SELECT 1 FROM "City" c WHERE c."name" = p.pname);
|
||||
|
||||
UPDATE "City" c
|
||||
SET "parentId" = (
|
||||
SELECT p."id" FROM "City" p
|
||||
WHERE p."id" <> c."id"
|
||||
AND p."name" = CASE WHEN btrim(c."province") LIKE 'استان%' THEN btrim(c."province") ELSE 'استان ' || btrim(c."province") END
|
||||
ORDER BY p."id"
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE c."province" IS NOT NULL AND btrim(c."province") <> '';
|
||||
|
||||
ALTER TABLE "City" DROP COLUMN "province";
|
||||
|
||||
CREATE INDEX "City_parentId_idx" ON "City"("parentId");
|
||||
|
||||
ALTER TABLE "City" ADD CONSTRAINT "City_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "City"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -118,10 +118,15 @@ enum LedgerSourceType {
|
||||
// Identity
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Legacy `cities` is a tree (country > province > city > district) and so is
|
||||
/// this: a city hangs under its `parent`. In the admin pickers a top-level row
|
||||
/// (the country) is a heading, not selectable (henza_admin citiesModel.js:38).
|
||||
model City {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
province String?
|
||||
parentId String?
|
||||
parent City? @relation("CityTree", fields: [parentId], references: [id], onDelete: Restrict)
|
||||
children City[] @relation("CityTree")
|
||||
/// Legacy cities.percent / percent_aux — per-city wage surcharges the city
|
||||
/// screen edits through GET cities/get-percent/{id} and get-percent-aux/{id}.
|
||||
percent Decimal? @db.Decimal(6, 3)
|
||||
@@ -131,6 +136,8 @@ model City {
|
||||
branches Branch[]
|
||||
preOrders PreOrder[]
|
||||
salesExpertProfiles SalesExpertProfile[]
|
||||
|
||||
@@index([parentId])
|
||||
}
|
||||
|
||||
model AdminUser {
|
||||
|
||||
+15
-4
@@ -113,11 +113,22 @@ async function main() {
|
||||
}
|
||||
|
||||
// ── Cities ────────────────────────────────────────────────────────────
|
||||
// A tree, as legacy's: country > province > city. The country is a heading
|
||||
// in the admin pickers; provinces and cities are selectable.
|
||||
const city = (id: string, name: string, parentId: string | null) =>
|
||||
prisma.city.upsert({ where: { id }, update: {}, create: { id, name, parentId } });
|
||||
await city('city-iran', 'ایران', null);
|
||||
await Promise.all([
|
||||
city('city-prov-tehran', 'استان تهران', 'city-iran'),
|
||||
city('city-prov-khorasan-razavi', 'استان خراسان رضوی', 'city-iran'),
|
||||
city('city-prov-isfahan', 'استان اصفهان', 'city-iran'),
|
||||
city('city-prov-east-azarbaijan', 'استان آذربایجان شرقی', 'city-iran'),
|
||||
]);
|
||||
const [tehran, mashhad, isfahan, tabriz] = await Promise.all([
|
||||
prisma.city.upsert({ where: { id: 'city-tehran' }, update: {}, create: { id: 'city-tehran', name: 'تهران', province: 'تهران' } }),
|
||||
prisma.city.upsert({ where: { id: 'city-mashhad' }, update: {}, create: { id: 'city-mashhad', name: 'مشهد', province: 'خراسان رضوی' } }),
|
||||
prisma.city.upsert({ where: { id: 'city-isfahan' }, update: {}, create: { id: 'city-isfahan', name: 'اصفهان', province: 'اصفهان' } }),
|
||||
prisma.city.upsert({ where: { id: 'city-tabriz' }, update: {}, create: { id: 'city-tabriz', name: 'تبریز', province: 'آذربایجان شرقی' } }),
|
||||
city('city-tehran', 'تهران', 'city-prov-tehran'),
|
||||
city('city-mashhad', 'مشهد', 'city-prov-khorasan-razavi'),
|
||||
city('city-isfahan', 'اصفهان', 'city-prov-isfahan'),
|
||||
city('city-tabriz', 'تبریز', 'city-prov-east-azarbaijan'),
|
||||
]);
|
||||
|
||||
// ── Branches & workshop ──────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user