admin user and ui panel

This commit is contained in:
s.esmaeili
2026-09-21 08:32:31 +03:30
parent 79226c9f13
commit ef1d4638cc
341 changed files with 2270 additions and 657 deletions
@@ -0,0 +1,11 @@
-- AlterTable
ALTER TABLE "ProductVariant" ADD COLUMN "imageId" TEXT;
-- CreateIndex
CREATE INDEX "ProductVariant_imageId_idx" ON "ProductVariant"("imageId");
-- AddForeignKey
-- ON DELETE SET NULL: removing a gallery image must never cascade into the
-- variant, because OrderItem rows reference variants and losing one would
-- break order history. The variant simply loses its picture.
ALTER TABLE "ProductVariant" ADD CONSTRAINT "ProductVariant_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "ProductImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,36 @@
-- Sales agents ("کارشناس فروش") are modelled as AdminUser rows
-- (role=SALES_EXPERT), not Customer rows — see admin-users.service.ts and
-- admin-customers.service.ts's listReagents(). ReagentCustomer.vue used to
-- post agent data at the Customer endpoints instead, so agents ended up as
-- Customer rows (kind SALES_AGENT), which then leaked into every
-- Customer-backed list, including "لیست مشتریان تک" (listSingle had no kind
-- filter at all). Fixed at the application layer alongside this migration;
-- this migration only adds what that fix needs:
--
-- 1. CustomerKind.RETAIL — legacy kept retail ("تک") customers in an
-- entirely separate table (SingleCustomer/`login`) that could never
-- contain a wholesale/agent/shopkeeper/banker row. The redesigned
-- schema folded everything into one Customer model, so listSingle()
-- needs an explicit kind to filter on to reproduce that separation.
-- 2. AdminUser gets the handful of Customer-only columns
-- (ReagentCustomer.vue) that a sales agent's form actually collects:
-- avatar, address, store/pasazh name, member code. Only ever set when
-- role=SALES_EXPERT; every other AdminUser leaves them null.
-- AlterEnum
ALTER TYPE "CustomerKind" ADD VALUE IF NOT EXISTS 'RETAIL';
-- AlterTable
ALTER TABLE "AdminUser"
ADD COLUMN "avatarFileId" TEXT,
ADD COLUMN "address" TEXT,
ADD COLUMN "storeName" TEXT,
ADD COLUMN "pasazhName" TEXT,
ADD COLUMN "memberCode" TEXT;
-- CreateIndex
CREATE INDEX "AdminUser_avatarFileId_idx" ON "AdminUser"("avatarFileId");
-- AddForeignKey
ALTER TABLE "AdminUser" ADD CONSTRAINT "AdminUser_avatarFileId_fkey"
FOREIGN KEY ("avatarFileId") REFERENCES "UploadedFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+47 -4
View File
@@ -4,7 +4,7 @@
// All money/weight fields use Decimal, never Float, to avoid rounding drift in debt math.
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
// native = local Windows/macOS; debian-openssl-3.0.x = node:20-bookworm-slim (Arvan)
binaryTargets = ["native", "debian-openssl-3.0.x"]
}
@@ -28,12 +28,27 @@ enum AdminRole {
/// Legacy Customer.type — what kind of wholesale customer this is. Values and
/// labels taken verbatim from bulkCustomer.vue's userTypes list.
///
/// SALES_AGENT is a LEGACY value only: legacy stored a sales agent as a
/// Customer row with type=1, but the redesigned system models a sales agent
/// as an AdminUser (role SALES_EXPERT) instead — see admin-users.service.ts
/// and admin-customers.service.ts's listReagents()/create(). No Customer row
/// should be created with kind SALES_AGENT going forward; the value is kept
/// only so legacyTypeToKind()/kindToLegacyType() stay total functions and any
/// already-migrated data keeps a readable label.
///
/// RETAIL marks a "تک" (single/retail) customer — legacy kept these in an
/// entirely separate table (SingleCustomer/`login`) that could never contain
/// a wholesale/agent/shopkeeper/banker row. The redesigned schema folded both
/// into one Customer model, so this value is what listSingle() now filters on
/// to reproduce that same separation.
enum CustomerKind {
SALES_AGENT // 1 کارشناس فروش
SALES_AGENT // legacy 1 کارشناس فروش — superseded by AdminRole.SALES_EXPERT, do not use for new rows
APP_SHOPKEEPER // 2 مغازه دار اپلیکیشن
WALK_IN_SHOPKEEPER // 3 مغازه دار حضوری
BANKER // 4 بنکدار
WORKSHOP_SUPERVISOR // 5 سرپرست کارگاه
RETAIL // مشتری تک — legacy SingleCustomer
}
enum VerificationStep {
@@ -127,6 +142,18 @@ model AdminUser {
isActive Boolean @default(true)
createdAt DateTime @default(now())
/// Legacy stored a sales agent as a `customers` row with type=1, so
/// ReagentCustomer.vue's shared form collected these alongside the
/// wholesale-customer fields. A sales agent is now an AdminUser
/// (role=SALES_EXPERT), so these live here instead — only ever set for that
/// role; every other AdminUser leaves them null.
avatarFileId String?
avatarFile UploadedFile? @relation("AdminUserAvatar", fields: [avatarFileId], references: [id])
address String?
storeName String? // legacy store_name
pasazhName String? // legacy pasazh_name
memberCode String? // legacy mcode
referredCustomers Customer[] @relation("ReferralAgent")
supervisedWorkshops Workshop[] @relation("WorkshopSupervisor")
branchAllocations BranchAllocation[] @relation("AllocationAuthor")
@@ -139,6 +166,7 @@ model AdminUser {
@@index([role])
@@index([userTypeId])
@@index([avatarFileId])
}
model Customer {
@@ -329,6 +357,11 @@ model ProductImage {
/// گالری تصاویر سایت / گالری تصاویر موبایل) — GALLERY is the default so
/// existing rows and any generic "just attach an image" caller keep working.
role ProductImageRole @default(GALLERY)
/// Variants that display this image. A gallery image may back several
/// variants (one photo, several weights), which is why this is a plain
/// one-to-many and not a join table.
variants ProductVariant[]
}
enum ProductImageRole {
@@ -356,7 +389,16 @@ model ProductVariant {
isPreOrder Boolean @default(false)
isActive Boolean @default(true)
/// The gallery image shown for this variant, picked from its own product's
/// images — never a re-uploaded copy. onDelete: SetNull because dropping an
/// image must never cascade into the variant: OrderItem rows reference
/// variants, so losing one would break order history.
imageId String?
image ProductImage? @relation(fields: [imageId], references: [id], onDelete: SetNull)
orderItems OrderItem[]
@@index([imageId])
}
// ─────────────────────────────────────────────────────────────────────────
@@ -861,8 +903,9 @@ model UploadedFile {
uploadedByAdminUserId String?
uploadedByAdminUser AdminUser? @relation("FileUploader", fields: [uploadedByAdminUserId], references: [id])
createdAt DateTime @default(now())
customerAvatars Customer[] @relation("CustomerAvatar")
createdAt DateTime @default(now())
customerAvatars Customer[] @relation("CustomerAvatar")
adminUserAvatars AdminUser[] @relation("AdminUserAvatar")
@@index([purpose])
}