build: rebuild API release bundle for the admin/expert table split

app.js, seed.js, the Prisma client in release/vendor and the new migration
20261026120000_split_admin_table, built with `pnpm build:release` (API only;
the admin panel and user apps are unchanged by this work).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-10-25 23:55:07 +03:30
co-authored by Claude Sonnet 5.5
parent 06c8a5f434
commit e2ec30f241
10 changed files with 5403 additions and 3445 deletions
+211 -101
View File
@@ -90343,7 +90343,7 @@ var require_admin_uploads_service = __commonJS({
mimeType: file2.mimetype,
sizeBytes: file2.size,
purpose: purpose ?? null,
uploadedByAdminUserId: actorAdminUserId
uploadedByAdminId: actorAdminUserId
}
});
}
@@ -91521,7 +91521,7 @@ var require_ledger_service = __commonJS({
note: input.note,
confirmed,
confirmedAt: confirmed ? /* @__PURE__ */ new Date() : null,
postedByAdminUserId: input.postedByAdminUserId
postedByAdminId: input.postedByAdminId
}
});
if (confirmed) {
@@ -91546,7 +91546,7 @@ var require_ledger_service = __commonJS({
return entry;
const updated = await tx.ledgerEntry.update({
where: { id: entryId },
data: { confirmed: true, confirmedAt: /* @__PURE__ */ new Date(), postedByAdminUserId: adminUserId ?? entry.postedByAdminUserId }
data: { confirmed: true, confirmedAt: /* @__PURE__ */ new Date(), postedByAdminId: adminUserId ?? entry.postedByAdminId }
});
await this.applyToBalance(entry.customerId, entry.currency, entry.direction, entry.amount, tx);
return updated;
@@ -91660,7 +91660,7 @@ var require_ledger_service = __commonJS({
note: params.note,
confirmed: false,
// pending until an admin confirms — intentional deviation from legacy, which auto-confirms on creation
postedByAdminUserId: params.postedByAdminUserId
postedByAdminId: params.postedByAdminId
}, tx);
}
/**
@@ -91677,7 +91677,7 @@ var require_ledger_service = __commonJS({
amount: params.amount,
note: params.note,
confirmed: false,
postedByAdminUserId: params.postedByAdminUserId
postedByAdminId: params.postedByAdminId
}, tx);
}
async postAdjustment(params, tx = this.prisma) {
@@ -91689,7 +91689,7 @@ var require_ledger_service = __commonJS({
amount: params.amount,
note: params.note,
confirmed: true,
postedByAdminUserId: params.postedByAdminUserId
postedByAdminId: params.postedByAdminId
}, tx);
}
async getBalance(customerId) {
@@ -92254,7 +92254,7 @@ var require_orders_service = __commonJS({
}
});
await tx.orderStatusEvent.create({
data: { orderId, fromStatus: order.status, toStatus: newStatus, note, actorAdminUserId }
data: { orderId, fromStatus: order.status, toStatus: newStatus, note, actorAdminId: actorAdminUserId }
});
await this.ledger.syncOrderDebt(orderId, tx);
if (newStatus === client_1.OrderStatus.CANCELLED && order.status !== client_1.OrderStatus.CANCELLED) {
@@ -93827,7 +93827,7 @@ var require_admin_session_service = __commonJS({
await this.prisma.adminSession.create({
data: {
jti: input.jti,
adminUserId: input.adminUserId,
adminId: input.adminId,
iamSessionId: input.iamSessionId ?? null,
iamSub: input.iamSub ?? null,
ipAddress: input.ipAddress ?? null,
@@ -93863,8 +93863,8 @@ var require_admin_session_service = __commonJS({
return this.revokeWhere({ iamSub }, reason);
}
/** Revoke every session of a local admin (account disabled, password changed). */
async revokeAllForAdmin(adminUserId, reason = "revoked") {
return this.revokeWhere({ adminUserId }, reason);
async revokeAllForAdmin(adminId, reason = "revoked") {
return this.revokeWhere({ adminId }, reason);
}
async revokeWhere(where, reason) {
if (!this.enabled)
@@ -94003,7 +94003,7 @@ var require_auth_service = __commonJS({
if (isStaticAdmin) {
await this.ensureStaticAdmin();
}
const user = await this.prisma.adminUser.findUnique({
const user = await this.prisma.admin.findUnique({
where: { mobile: isStaticAdmin ? STATIC_ADMIN_MOBILE : dto.phone },
include: { userType: { include: { permissions: true } } }
});
@@ -94023,7 +94023,7 @@ var require_auth_service = __commonJS({
if (jti) {
await this.sessions.record({
jti,
adminUserId: user.id,
adminId: user.id,
iamSessionId: verified.identity?.sessionId ?? null,
iamSub: verified.identity?.sub ?? null,
ipAddress: clientIp ?? null,
@@ -94084,7 +94084,7 @@ var require_auth_service = __commonJS({
if (!claims.phoneNumber) {
throw new common_12.UnauthorizedException("\u0647\u0648\u06CC\u062A \u0645\u0634\u062A\u0631\u06A9 \u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06CC\u0644 \u0646\u062F\u0627\u0631\u062F");
}
const user = await this.prisma.adminUser.findFirst({
const user = await this.prisma.admin.findFirst({
where: { mobile: { in: mobileVariants(claims.phoneNumber) }, isActive: true },
include: { userType: { include: { permissions: true } } }
});
@@ -94128,7 +94128,7 @@ var require_auth_service = __commonJS({
if (jti) {
await this.sessions.record({
jti,
adminUserId: user.id,
adminId: user.id,
iamSessionId: identity?.sessionId ?? null,
iamSub: identity?.sub ?? null,
ipAddress: ip ?? null,
@@ -94152,7 +94152,7 @@ var require_auth_service = __commonJS({
const fullAccess = await this.prisma.userType.findUnique({ where: { id: 85 } });
const userTypeId = fullAccess ? 85 : null;
const passwordHash = await bcrypt.hash(STATIC_ADMIN_MOBILE, 10);
await this.prisma.adminUser.upsert({
await this.prisma.admin.upsert({
where: { mobile: STATIC_ADMIN_MOBILE },
update: { role: client_1.AdminRole.SUPER_ADMIN, userTypeId, branchId: null, isActive: true },
create: {
@@ -94230,7 +94230,7 @@ var require_auth_service = __commonJS({
* legacy. The guard's own checks use the pre-expanded list from JwtStrategy.
*/
async showData(adminId) {
const user = await this.prisma.adminUser.findUnique({
const user = await this.prisma.admin.findUnique({
where: { id: adminId },
include: { userType: { include: { permissions: true } } }
});
@@ -94644,7 +94644,7 @@ var require_jwt_strategy = __commonJS({
if (!await this.sessions.isActive(payload.jti)) {
throw new common_12.UnauthorizedException();
}
const user = await this.prisma.adminUser.findUnique({
const user = await this.prisma.admin.findUnique({
where: { id: payload.sub },
include: { userType: { include: { permissions: true } } }
});
@@ -95418,7 +95418,7 @@ var require_admin_orders_service = __commonJS({
customer: true,
branch: true,
items: { include: { productVariant: { include: { product: true } } } },
statusEvents: { include: { actorAdminUser: true }, orderBy: { createdAt: "asc" } }
statusEvents: { include: { actorAdmin: true }, orderBy: { createdAt: "asc" } }
}
});
if (!order)
@@ -96336,7 +96336,7 @@ var require_admin_branches_service = __commonJS({
/** Legacy `POST branch/table`. */
async listBranches(query) {
this.table.assertNotExport(query);
const aliases = { "user.name": "adminUsers.some.fullName" };
const aliases = { "user.name": "admins.some.fullName" };
const { where, orderBy, skip, take, page, limit } = this.table.build(query, {
searchable: ["name", "address", "phone"],
allowed: ["id", "name", "address", "phone", "isActive", "cityId", "city.name"],
@@ -96351,8 +96351,8 @@ var require_admin_branches_service = __commonJS({
take,
include: {
city: true,
adminUsers: { where: { isActive: true }, take: 1 },
_count: { select: { adminUsers: true, orders: true } }
admins: { where: { isActive: true }, take: 1 },
_count: { select: { admins: true, orders: true } }
}
}),
this.prisma.branch.count({ where })
@@ -96367,15 +96367,15 @@ var require_admin_branches_service = __commonJS({
city_id: b.cityId,
city: b.city ? { id: b.city.id, title: b.city.name } : null,
// legacy column key is `user.name` — the branch manager
user: b.adminUsers[0] ? { id: b.adminUsers[0].id, name: b.adminUsers[0].fullName } : null,
users_count: b._count.adminUsers,
user: b.admins[0] ? { id: b.admins[0].id, name: b.admins[0].fullName } : null,
users_count: b._count.admins,
orders_count: b._count.orders
})), total, query, page, limit);
}
async findBranch(id) {
const b = await this.prisma.branch.findUnique({
where: { id },
include: { city: true, adminUsers: { where: { isActive: true }, take: 1 } }
include: { city: true, admins: { where: { isActive: true }, take: 1 } }
});
if (!b)
throw new common_12.NotFoundException("\u0634\u0639\u0628\u0647 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
@@ -96386,8 +96386,8 @@ var require_admin_branches_service = __commonJS({
phone: b.phone,
is_active: b.isActive,
city_id: b.cityId,
user_id: b.adminUsers[0]?.id ?? null,
user: b.adminUsers[0] ? { id: b.adminUsers[0].id, name: b.adminUsers[0].fullName } : null
user_id: b.admins[0]?.id ?? null,
user: b.admins[0] ? { id: b.admins[0].id, name: b.admins[0].fullName } : null
};
}
/** Legacy `POST branch` — the manager is an EXISTING admin picked by id. */
@@ -96436,12 +96436,12 @@ var require_admin_branches_service = __commonJS({
*/
async assignBranchManager(branchId, userId) {
await this.prisma.$transaction(async (tx) => {
await tx.adminUser.updateMany({ where: { branchId }, data: { branchId: null } });
await tx.admin.updateMany({ where: { branchId }, data: { branchId: null } });
if (userId) {
const user = await tx.adminUser.findUnique({ where: { id: userId } });
const user = await tx.admin.findUnique({ where: { id: userId } });
if (!user)
throw new common_12.NotFoundException("\u06A9\u0627\u0631\u0628\u0631 \u0627\u0646\u062A\u062E\u0627\u0628\u200C\u0634\u062F\u0647 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
await tx.adminUser.update({ where: { id: userId }, data: { branchId } });
await tx.admin.update({ where: { id: userId }, data: { branchId } });
}
});
}
@@ -96564,25 +96564,25 @@ var require_admin_branches_service = __commonJS({
throw new common_12.BadRequestException("\u0645\u0648\u0628\u0627\u06CC\u0644 \u0633\u0631\u067E\u0631\u0633\u062A \u06A9\u0627\u0631\u06AF\u0627\u0647 \u0627\u0644\u0632\u0627\u0645\u06CC \u0627\u0633\u062A");
const fullName = `${fname} ${lname}`.trim();
if (currentId) {
const current = await this.prisma.adminUser.findUnique({ where: { id: currentId } });
const current = await this.prisma.admin.findUnique({ where: { id: currentId } });
if (current) {
if (phone !== current.mobile) {
const clash = await this.prisma.adminUser.findUnique({ where: { mobile: phone } });
const clash = await this.prisma.admin.findUnique({ where: { mobile: phone } });
if (clash && clash.id !== currentId) {
throw new common_12.BadRequestException("\u0627\u06CC\u0646 \u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06CC\u0644 \u0642\u0628\u0644\u0627\u064B \u062B\u0628\u062A \u0634\u062F\u0647 \u0627\u0633\u062A");
}
}
await this.prisma.adminUser.update({
await this.prisma.admin.update({
where: { id: currentId },
data: { fullName, mobile: phone }
});
return currentId;
}
}
const existing = await this.prisma.adminUser.findUnique({ where: { mobile: phone } });
const existing = await this.prisma.admin.findUnique({ where: { mobile: phone } });
if (existing)
return existing.id;
const created = await this.prisma.adminUser.create({
const created = await this.prisma.admin.create({
data: {
fullName,
mobile: phone,
@@ -96605,7 +96605,7 @@ var require_admin_branches_service = __commonJS({
}
/** The admin picker the branch form uses — legacy `GET user/list`. */
async userList() {
const rows = await this.prisma.adminUser.findMany({
const rows = await this.prisma.admin.findMany({
where: { isActive: true },
orderBy: { fullName: "asc" },
select: { id: true, fullName: true, mobile: true, branchId: true }
@@ -100173,7 +100173,7 @@ var require_admin_documents_service = __commonJS({
orderBy,
skip,
take,
include: { customer: true, postedByAdminUser: true }
include: { customer: true, postedByAdmin: true }
}),
this.prisma.ledgerEntry.count({ where: scoped })
]);
@@ -100212,7 +100212,7 @@ var require_admin_documents_service = __commonJS({
lname: e.customer.lastName ?? "",
mobile: e.customer.mobile
},
posted_by: e.postedByAdminUser ? e.postedByAdminUser.fullName : null,
posted_by: e.postedByAdmin ? e.postedByAdmin.fullName : null,
caj: e.createdAt,
uaj: e.createdAt
};
@@ -100221,7 +100221,7 @@ var require_admin_documents_service = __commonJS({
async findOne(id) {
const e = await this.prisma.ledgerEntry.findUnique({
where: { id },
include: { customer: true, postedByAdminUser: true }
include: { customer: true, postedByAdmin: true }
});
if (!e)
throw new common_12.NotFoundException("\u0633\u0646\u062F \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
@@ -100250,7 +100250,7 @@ var require_admin_documents_service = __commonJS({
karatPerMille: data.ayar ? Number(data.ayar) : void 0,
pricePerGramRial: data.price_per_gram ? String(data.price_per_gram) : void 0,
note: data.note ?? void 0,
postedByAdminUserId: actorAdminUserId
postedByAdminId: actorAdminUserId
});
await this.prisma.ledgerEntry.update({
where: { id: entry.id },
@@ -101292,7 +101292,7 @@ var require_admin_users_service = __commonJS({
caj: "createdAt",
active: "isActive"
};
/** Legacy `GET users` — the admin user grid. */
/** Legacy `GET users` — the admin user grid (panel staff only). */
async list(query) {
this.table.assertNotExport(query);
const mapField = (f) => f ? AdminUsersService_1.FIELD_ALIASES[f] ?? f : f;
@@ -101319,14 +101319,14 @@ var require_admin_users_service = __commonJS({
AND: [where, { mobile: { not: super_admin_1.SUPER_ADMIN_MOBILE } }]
};
const [rows, total] = await Promise.all([
this.prisma.adminUser.findMany({
this.prisma.admin.findMany({
where: visible,
orderBy,
skip,
take,
include: { userType: true, branch: true, avatarFile: true }
include: { userType: true, branch: true }
}),
this.prisma.adminUser.count({ where: visible })
this.prisma.admin.count({ where: visible })
]);
return this.table.respond(rows.map((r) => this.toRow(r)), total, query, page, limit);
}
@@ -101345,20 +101345,20 @@ var require_admin_users_service = __commonJS({
user_type: u.userType ? { id: u.userType.id, title: u.userType.title } : null,
branch_id: u.branchId,
branch: u.branch ? { id: u.branch.id, name: u.branch.name } : null,
percent: u.wagePercent,
percent: u.wagePercent ?? null,
active: u.isActive ? 1 : -1,
is_active: u.isActive,
caj: u.createdAt,
// Sales-agent fields (ReagentCustomer.vue) — set only for role=SALES_EXPERT,
// null for every other admin. Same shapes admin-customers.service.ts used
// null for every admin. Same shapes admin-customers.service.ts used
// when this data still lived on Customer, so the form binds unchanged.
address: u.address,
store_name: u.storeName,
pasazh_name: u.pasazhName,
mcode: u.memberCode,
image_id: u.avatarFileId,
address: u.address ?? null,
store_name: u.storeName ?? null,
pasazh_name: u.pasazhName ?? null,
mcode: u.memberCode ?? null,
image_id: u.avatarFileId ?? null,
image: u.avatarFile ? { id: u.avatarFile.id, image: u.avatarFile.url } : null,
// D17 — from SalesExpertProfile. Null for a non-expert, and null-safe for
// D17 — from SalesExpertProfile. Null for an admin, and null-safe for
// an expert whose profile row is somehow missing.
city_id: u.salesExpertProfile?.cityId ?? null,
city: u.salesExpertProfile?.city ? { id: u.salesExpertProfile.city.id, title: u.salesExpertProfile.city.name } : null,
@@ -101367,9 +101367,22 @@ var require_admin_users_service = __commonJS({
ncode: u.salesExpertProfile?.nationalCode ?? null
};
}
async findOne(id) {
const u = await this.prisma.adminUser.findUnique({
/** A panel admin by id, or null. The support super admin is never found. */
async findAdmin(id) {
const admin = await this.prisma.admin.findUnique({
where: { id },
include: { userType: true, branch: true }
});
return admin && !(0, super_admin_1.isSuperAdmin)(admin.mobile) ? admin : null;
}
/**
* A sales expert by id, or null. `role` is part of the lookup on purpose:
* "AdminUser" still holds the pre-split copies of the admins (dormant), and
* those must never be reachable as experts either.
*/
async findExpert(id) {
const expert = await this.prisma.adminUser.findFirst({
where: { id, role: client_1.AdminRole.SALES_EXPERT },
// D17 — the edit form needs the sales-expert fields too.
include: {
userType: true,
@@ -101378,9 +101391,16 @@ var require_admin_users_service = __commonJS({
salesExpertProfile: { include: { city: true } }
}
});
if (!u || (0, super_admin_1.isSuperAdmin)(u.mobile))
throw new common_12.NotFoundException("\u06A9\u0627\u0631\u0628\u0631 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
return this.toRow(u);
return expert && !(0, super_admin_1.isSuperAdmin)(expert.mobile) ? expert : null;
}
async findOne(id) {
const admin = await this.findAdmin(id);
if (admin)
return this.toRow(admin);
const expert = await this.findExpert(id);
if (expert)
return this.toRow(expert);
throw new common_12.NotFoundException("\u06A9\u0627\u0631\u0628\u0631 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
}
/**
* Legacy `POST users`.
@@ -101389,6 +101409,8 @@ var require_admin_users_service = __commonJS({
* caller omitted it, which silently turned every admin created without an
* explicit role into a sales agent regardless of the access level ("نوع
* کاربری") actually chosen. UserCreate.vue now always sends one.
*
* SALES_EXPERT goes to the `AdminUser` table, every other role to `Admin`.
*/
async create(data) {
const firstName = String(data.fname ?? data.name ?? "").trim();
@@ -101405,6 +101427,31 @@ var require_admin_users_service = __commonJS({
throw new common_12.BadRequestException("\u0631\u0645\u0632 \u0639\u0628\u0648\u0631 \u0627\u0644\u0632\u0627\u0645\u06CC \u0627\u0633\u062A");
if (!role)
throw new common_12.BadRequestException("\u0646\u0642\u0634 \u06A9\u0627\u0631\u0628\u0631 \u0627\u0644\u0632\u0627\u0645\u06CC \u0627\u0633\u062A");
if (!Object.values(client_1.AdminRole).includes(role))
throw new common_12.BadRequestException("\u0646\u0642\u0634 \u06A9\u0627\u0631\u0628\u0631 \u0645\u0639\u062A\u0628\u0631 \u0646\u06CC\u0633\u062A");
if (role === client_1.AdminRole.SALES_EXPERT) {
return this.createExpert({ data, firstName, lastName, fullName, mobile, password });
}
const existing = await this.prisma.admin.findUnique({ where: { mobile } });
if (existing)
throw new common_12.BadRequestException("\u0627\u06CC\u0646 \u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06CC\u0644 \u0642\u0628\u0644\u0627\u064B \u062B\u0628\u062A \u0634\u062F\u0647 \u0627\u0633\u062A");
return this.prisma.admin.create({
data: {
fullName,
firstName: firstName || null,
lastName: lastName || null,
mobile,
passwordHash: await bcrypt.hash(password, 10),
role,
userTypeId: data.ut !== void 0 && data.ut !== null ? Number(data.ut) : null,
branchId: data.branch_id || null,
isActive: data.is_active !== void 0 ? Boolean(data.is_active) : true
},
include: { userType: true, branch: true }
});
}
async createExpert(input) {
const { data, firstName, lastName, fullName, mobile, password } = input;
const existing = await this.prisma.adminUser.findUnique({ where: { mobile } });
if (existing)
throw new common_12.BadRequestException("\u0627\u06CC\u0646 \u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06CC\u0644 \u0642\u0628\u0644\u0627\u064B \u062B\u0628\u062A \u0634\u062F\u0647 \u0627\u0633\u062A");
@@ -101415,13 +101462,12 @@ var require_admin_users_service = __commonJS({
lastName: lastName || null,
mobile,
passwordHash: await bcrypt.hash(password, 10),
role,
role: client_1.AdminRole.SALES_EXPERT,
userTypeId: data.ut !== void 0 && data.ut !== null ? Number(data.ut) : null,
branchId: data.branch_id || null,
wagePercent: data.percent !== void 0 && data.percent !== null ? new client_1.Prisma.Decimal(String(data.percent)) : null,
isActive: data.is_active !== void 0 ? Boolean(data.is_active) : true,
// Sales-agent-only fields (ReagentCustomer.vue); undefined for every
// other role, which Prisma leaves at column default (null).
// Sales-agent-only fields (ReagentCustomer.vue).
address: data.address || null,
storeName: data.store_name || null,
pasazhName: data.pasazh_name || null,
@@ -101429,7 +101475,7 @@ var require_admin_users_service = __commonJS({
avatarFileId: data.image_id || null,
// D17 — a sales expert always gets its profile row, so the grid's city
// and credit-limit columns have somewhere to read from immediately.
...role === "SALES_EXPERT" ? { salesExpertProfile: { create: this.salesExpertProfileData(data) } } : {}
salesExpertProfile: { create: this.salesExpertProfileData(data) }
},
include: {
userType: true,
@@ -101456,9 +101502,65 @@ var require_admin_users_service = __commonJS({
}
/** Legacy `PUT users/{id}`. */
async update(id, data) {
const existing = await this.prisma.adminUser.findUnique({ where: { id } });
if (!existing || (0, super_admin_1.isSuperAdmin)(existing.mobile))
throw new common_12.NotFoundException("\u06A9\u0627\u0631\u0628\u0631 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
const admin = await this.findAdmin(id);
if (admin)
return this.updateAdmin(admin, data);
const expert = await this.findExpert(id);
if (expert)
return this.updateExpert(expert, data);
throw new common_12.NotFoundException("\u06A9\u0627\u0631\u0628\u0631 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
}
async updateAdmin(existing, data) {
const id = existing.id;
const patch = {};
if (data.fname !== void 0 || data.lname !== void 0) {
const firstName = String(data.fname ?? existing.firstName ?? "").trim();
const lastName = String(data.lname ?? existing.lastName ?? "").trim();
patch.firstName = firstName || null;
patch.lastName = lastName || null;
} else if (data.name !== void 0) {
patch.fullName = String(data.name);
}
if (data.mobile !== void 0) {
const mobile = (0, digits_1.faToEn)(String(data.mobile));
if (mobile !== existing.mobile) {
const clash = await this.prisma.admin.findUnique({ where: { mobile } });
if (clash)
throw new common_12.BadRequestException("\u0627\u06CC\u0646 \u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06CC\u0644 \u0642\u0628\u0644\u0627\u064B \u062B\u0628\u062A \u0634\u062F\u0647 \u0627\u0633\u062A");
}
patch.mobile = mobile;
}
if (data.password)
patch.passwordHash = await bcrypt.hash(String(data.password), 10);
if (data.role !== void 0) {
const role = data.role;
if (!Object.values(client_1.AdminRole).includes(role))
throw new common_12.BadRequestException("\u0646\u0642\u0634 \u06A9\u0627\u0631\u0628\u0631 \u0645\u0639\u062A\u0628\u0631 \u0646\u06CC\u0633\u062A");
if (role === client_1.AdminRole.SALES_EXPERT) {
throw new common_12.BadRequestException("\u0646\u0642\u0634 \u06A9\u0627\u0631\u0634\u0646\u0627\u0633 \u0641\u0631\u0648\u0634 \u0628\u0631\u0627\u06CC \u0627\u062F\u0645\u06CC\u0646 \u0642\u0627\u0628\u0644 \u062A\u0646\u0638\u06CC\u0645 \u0646\u06CC\u0633\u062A\u061B \u06A9\u0627\u0631\u0634\u0646\u0627\u0633 \u0631\u0627 \u0627\u0632 \u0628\u062E\u0634 \u06A9\u0627\u0631\u0634\u0646\u0627\u0633\u0627\u0646 \u0628\u0633\u0627\u0632\u06CC\u062F");
}
patch.role = role;
}
if (data.ut !== void 0) {
patch.userType = data.ut ? { connect: { id: Number(data.ut) } } : { disconnect: true };
}
if (data.branch_id !== void 0) {
patch.branch = data.branch_id ? { connect: { id: String(data.branch_id) } } : { disconnect: true };
}
if (data.is_active !== void 0)
patch.isActive = Boolean(data.is_active);
const updated = await this.prisma.admin.update({
where: { id },
data: patch,
include: { userType: true, branch: true }
});
if (patch.isActive === false || patch.passwordHash !== void 0) {
await this.sessions.revokeAllForAdmin(id, patch.isActive === false ? "account_disabled" : "password_changed");
}
return updated;
}
async updateExpert(existing, data) {
const id = existing.id;
const patch = {};
if (data.fname !== void 0 || data.lname !== void 0) {
const firstName = String(data.fname ?? existing.firstName ?? "").trim();
@@ -101479,8 +101581,9 @@ var require_admin_users_service = __commonJS({
}
if (data.password)
patch.passwordHash = await bcrypt.hash(String(data.password), 10);
if (data.role !== void 0)
patch.role = data.role;
if (data.role !== void 0 && data.role !== client_1.AdminRole.SALES_EXPERT) {
throw new common_12.BadRequestException("\u0646\u0642\u0634 \u06A9\u0627\u0631\u0634\u0646\u0627\u0633 \u0641\u0631\u0648\u0634 \u0642\u0627\u0628\u0644 \u062A\u063A\u06CC\u06CC\u0631 \u0646\u06CC\u0633\u062A");
}
if (data.ut !== void 0) {
patch.userType = data.ut ? { connect: { id: Number(data.ut) } } : { disconnect: true };
}
@@ -101503,9 +101606,8 @@ var require_admin_users_service = __commonJS({
if (data.image_id !== void 0) {
patch.avatarFile = data.image_id ? { connect: { id: String(data.image_id) } } : { disconnect: true };
}
const effectiveRole = patch.role ?? existing.role;
const touchesProfile = data.city_id !== void 0 || data.gram_level !== void 0 || data.phone !== void 0 || data.ncode !== void 0;
if (effectiveRole === "SALES_EXPERT" && (touchesProfile || patch.role !== void 0)) {
if (touchesProfile) {
const profile = this.salesExpertProfileData(data);
patch.salesExpertProfile = {
upsert: {
@@ -101521,7 +101623,7 @@ var require_admin_users_service = __commonJS({
}
};
}
const updated = await this.prisma.adminUser.update({
return this.prisma.adminUser.update({
where: { id },
data: patch,
include: {
@@ -101531,10 +101633,6 @@ var require_admin_users_service = __commonJS({
salesExpertProfile: { include: { city: true } }
}
});
if (patch.isActive === false || patch.passwordHash !== void 0) {
await this.sessions.revokeAllForAdmin(id, patch.isActive === false ? "account_disabled" : "password_changed");
}
return updated;
}
/**
* Legacy `DELETE users/{id}`.
@@ -101544,21 +101642,28 @@ var require_admin_users_service = __commonJS({
* audit trail. Deactivation is offered instead. Legacy deleted regardless.
*/
async remove(id, actorId) {
const u = await this.prisma.adminUser.findUnique({
const admin = await this.prisma.admin.findUnique({
where: { id },
include: {
_count: { select: { postedLedgerEntries: true, orderStatusEvents: true } }
}
});
if (!u || (0, super_admin_1.isSuperAdmin)(u.mobile))
if (admin) {
if ((0, super_admin_1.isSuperAdmin)(admin.mobile))
throw new common_12.NotFoundException("\u06A9\u0627\u0631\u0628\u0631 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
if (id === actorId) {
throw new common_12.BadRequestException("\u0646\u0645\u06CC\u200C\u062A\u0648\u0627\u0646\u06CC\u062F \u062D\u0633\u0627\u0628 \u06A9\u0627\u0631\u0628\u0631\u06CC \u062E\u0648\u062F\u062A\u0627\u0646 \u0631\u0627 \u062D\u0630\u0641 \u06A9\u0646\u06CC\u062F");
}
const refs = admin._count.postedLedgerEntries + admin._count.orderStatusEvents;
if (refs > 0) {
throw new common_12.BadRequestException(`\u062D\u0630\u0641 \u0627\u06CC\u0646 \u06A9\u0627\u0631\u0628\u0631 \u0645\u0645\u06A9\u0646 \u0646\u06CC\u0633\u062A\u060C \u0686\u0648\u0646 ${refs} \u0633\u0646\u062F \u06CC\u0627 \u062A\u063A\u06CC\u06CC\u0631 \u0648\u0636\u0639\u06CC\u062A \u0628\u0647 \u0646\u0627\u0645 \u0627\u0648 \u062B\u0628\u062A \u0634\u062F\u0647 \u0627\u0633\u062A. \u0645\u06CC\u200C\u062A\u0648\u0627\u0646\u06CC\u062F \u063A\u06CC\u0631\u0641\u0639\u0627\u0644\u0634 \u06A9\u0646\u06CC\u062F.`);
}
await this.prisma.admin.delete({ where: { id } });
return { success: true };
}
const expert = await this.findExpert(id);
if (!expert)
throw new common_12.NotFoundException("\u06A9\u0627\u0631\u0628\u0631 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
if (id === actorId) {
throw new common_12.BadRequestException("\u0646\u0645\u06CC\u200C\u062A\u0648\u0627\u0646\u06CC\u062F \u062D\u0633\u0627\u0628 \u06A9\u0627\u0631\u0628\u0631\u06CC \u062E\u0648\u062F\u062A\u0627\u0646 \u0631\u0627 \u062D\u0630\u0641 \u06A9\u0646\u06CC\u062F");
}
const refs = u._count.postedLedgerEntries + u._count.orderStatusEvents;
if (refs > 0) {
throw new common_12.BadRequestException(`\u062D\u0630\u0641 \u0627\u06CC\u0646 \u06A9\u0627\u0631\u0628\u0631 \u0645\u0645\u06A9\u0646 \u0646\u06CC\u0633\u062A\u060C \u0686\u0648\u0646 ${refs} \u0633\u0646\u062F \u06CC\u0627 \u062A\u063A\u06CC\u06CC\u0631 \u0648\u0636\u0639\u06CC\u062A \u0628\u0647 \u0646\u0627\u0645 \u0627\u0648 \u062B\u0628\u062A \u0634\u062F\u0647 \u0627\u0633\u062A. \u0645\u06CC\u200C\u062A\u0648\u0627\u0646\u06CC\u062F \u063A\u06CC\u0631\u0641\u0639\u0627\u0644\u0634 \u06A9\u0646\u06CC\u062F.`);
}
await this.prisma.adminUser.delete({ where: { id } });
return { success: true };
}
@@ -101567,12 +101672,12 @@ var require_admin_users_service = __commonJS({
async listUserTypes() {
const rows = await this.prisma.userType.findMany({
orderBy: { id: "asc" },
include: { _count: { select: { adminUsers: true, permissions: true } } }
include: { _count: { select: { admins: true, adminUsers: true, permissions: true } } }
});
return rows.map((t) => ({
id: t.id,
title: t.title,
users_count: t._count.adminUsers,
users_count: t._count.admins + t._count.adminUsers,
permissions_count: t._count.permissions
}));
}
@@ -101645,14 +101750,19 @@ var require_admin_users_service = __commonJS({
}
/** Legacy `GET users/show` equivalent for the "اطلاعات من" screen. */
async myData(adminId) {
return this.findOne(adminId);
const admin = await this.findAdmin(adminId);
if (!admin)
throw new common_12.NotFoundException("\u06A9\u0627\u0631\u0628\u0631 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
return this.toRow(admin);
}
/** The AdminRole picker for the user form. */
/**
* The AdminRole picker for the staff form. SALES_EXPERT is not offered: an
* expert is a different table and is created from the reagent form.
*/
roleOptions() {
return [
{ value: client_1.AdminRole.SUPER_ADMIN, title: "\u0645\u062F\u06CC\u0631 \u0627\u0631\u0634\u062F" },
{ value: client_1.AdminRole.SYSTEM_APPROVER, title: "\u062A\u0627\u06CC\u06CC\u062F \u0645\u062F\u06CC\u0631 \u0633\u06CC\u0633\u062A\u0645" },
{ value: client_1.AdminRole.SALES_EXPERT, title: "\u06A9\u0627\u0631\u0634\u0646\u0627\u0633 \u0641\u0631\u0648\u0634" },
{ value: client_1.AdminRole.WORKSHOP_SUPERVISOR, title: "\u0633\u0631\u067E\u0631\u0633\u062A \u06A9\u0627\u0631\u06AF\u0627\u0647" },
{ value: client_1.AdminRole.ACCOUNTANT, title: "\u062D\u0633\u0627\u0628\u062F\u0627\u0631" }
];
@@ -101915,7 +102025,7 @@ var require_admin_invoices_service = __commonJS({
"createdAt",
"branchId",
"branch.name",
"allocatorAdminUser.fullName",
"allocatorAdmin.fullName",
// legacy filter keys
"name",
"caj"
@@ -101923,7 +102033,7 @@ var require_admin_invoices_service = __commonJS({
static FIELD_ALIASES = {
name: "branch.name",
caj: "createdAt",
"allocator_user.name": "allocatorAdminUser.fullName"
"allocator_user.name": "allocatorAdmin.fullName"
};
/** Legacy `POST branch/product/table`. */
async list(query) {
@@ -101947,7 +102057,7 @@ var require_admin_invoices_service = __commonJS({
take,
include: {
branch: true,
allocatorAdminUser: true,
allocatorAdmin: true,
items: { include: { product: true } }
}
}),
@@ -101966,7 +102076,7 @@ var require_admin_invoices_service = __commonJS({
sum_weight: sumWeight,
count_item: a.items.length,
note: a.note,
allocator_user: a.allocatorAdminUser ? { id: a.allocatorAdminUser.id, name: a.allocatorAdminUser.fullName } : null,
allocator_user: a.allocatorAdmin ? { id: a.allocatorAdmin.id, name: a.allocatorAdmin.fullName } : null,
caj: a.createdAt,
products: a.items.map((it) => ({
id: it.productId,
@@ -101983,7 +102093,7 @@ var require_admin_invoices_service = __commonJS({
where: { id },
include: {
branch: true,
allocatorAdminUser: true,
allocatorAdmin: true,
items: { include: { product: true } }
}
});
@@ -102017,7 +102127,7 @@ var require_admin_invoices_service = __commonJS({
const allocation = await this.prisma.branchAllocation.create({
data: {
branchId,
allocatorAdminUserId: actorId,
allocatorAdminId: actorId,
note: data.note ?? null,
items: {
create: products.map((p) => ({
@@ -102446,7 +102556,7 @@ var require_admin_misc_service = __commonJS({
orderBy,
skip,
take,
include: { sentByAdminUser: true }
include: { sentByAdmin: true }
}),
this.prisma.smsMessage.count({ where })
]);
@@ -102456,7 +102566,7 @@ var require_admin_misc_service = __commonJS({
text: s.body,
mobile: s.recipients,
sent_count: s.sentCount,
sender: s.sentByAdminUser?.fullName ?? null,
sender: s.sentByAdmin?.fullName ?? null,
caj: s.createdAt
})), total, query, page, limit);
}
@@ -102483,7 +102593,7 @@ var require_admin_misc_service = __commonJS({
recipients: recipients.join(","),
body,
sentCount: recipients.length,
sentByAdminUserId: actorId
sentByAdminId: actorId
}
});
}
@@ -102566,7 +102676,7 @@ var require_admin_misc_service = __commonJS({
title,
body,
audience,
sentByAdminUserId: actorId,
sentByAdminId: actorId,
recipients: { create: customers.map((c) => ({ customerId: c.id })) }
},
include: { _count: { select: { recipients: true } } }
@@ -102589,7 +102699,7 @@ var require_admin_misc_service = __commonJS({
title,
body,
audience: client_1.MessageAudience.SINGLE_NUMBER,
sentByAdminUserId: actorId,
sentByAdminId: actorId,
recipients: { create: [{ customerId }] }
}
});
@@ -102599,7 +102709,7 @@ var require_admin_misc_service = __commonJS({
const n = await this.prisma.notification.findUnique({
where: { id },
include: {
sentByAdminUser: true,
sentByAdmin: true,
recipients: { include: { customer: true } }
}
});
@@ -102611,7 +102721,7 @@ var require_admin_misc_service = __commonJS({
body: n.body,
text: n.body,
audience: n.audience,
sender: n.sentByAdminUser?.fullName ?? null,
sender: n.sentByAdmin?.fullName ?? null,
caj: n.createdAt,
recipients: n.recipients.map((r) => ({
id: r.id,
@@ -102638,7 +102748,7 @@ var require_admin_misc_service = __commonJS({
skip,
take,
include: {
sentByAdminUser: true,
sentByAdmin: true,
_count: { select: { recipients: true } }
}
}),
@@ -102649,7 +102759,7 @@ var require_admin_misc_service = __commonJS({
title: n.title,
text: n.body,
audience: n.audience,
sender: n.sentByAdminUser?.fullName ?? null,
sender: n.sentByAdmin?.fullName ?? null,
total_count: n._count.recipients,
caj: n.createdAt
})), total, query, page, limit);
@@ -0,0 +1,167 @@
-- Split panel staff out of "AdminUser" into their own table, "Admin".
--
-- WHY
-- ---
-- "AdminUser" held two unrelated kinds of person: panel staff and sales
-- experts (role SALES_EXPERT). Because the mobile is unique per table, one
-- number could not be both an admin and an expert. Staff belong to the ADMIN
-- PANEL only; experts belong to the apps. They are now separate tables, each
-- unique on its own.
--
-- SAFETY — READ BEFORE RUNNING ON PRODUCTION
-- ------------------------------------------
-- This migration NEVER modifies, deletes or locks "AdminUser" (the sales-expert
-- table; ~12,000 rows in production). It only
-- 1. creates "Admin" and its indexes/foreign keys,
-- 2. COPIES the non-expert rows from "AdminUser" into it, keeping their ids,
-- 3. re-points the "who did this" foreign keys from "AdminUser" to "Admin".
--
-- * Ids are preserved, so every issued panel token (its `sub` is the id) and
-- every existing ledger / status / SMS / upload row keeps resolving.
-- * The old non-expert rows are LEFT in "AdminUser", dormant. The code no
-- longer reads them as admins. Remove them later, in a separate release, only
-- once the split has been verified.
-- * Re-runnable: every statement is guarded (IF NOT EXISTS / ON CONFLICT / drop
-- then add), so a half-applied run can simply be run again.
-- * No data is moved on the big tables (LedgerEntry, OrderStatusEvent, …):
-- the existing columns keep their names and values; only the constraint's
-- target changes.
--
-- The re-pointed foreign keys are added NOT VALID (checked for new/changed rows
-- immediately, existing rows not scanned under a heavy lock) and then validated
-- at the end. A row that references an id that is not an admin — only possible
-- if a sales expert once appears as an actor — leaves that one constraint
-- NOT VALID and prints a NOTICE instead of failing the deploy. See
-- docs/ADMIN-TABLE-SPLIT.md for the check query.
--
-- ROLLBACK: deploy the previous code. Nothing was deleted, so the old code
-- still finds every admin in "AdminUser". Admins created AFTER the split exist
-- only in "Admin" and would need re-creating in the old system.
-- ── 1. The table ───────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS "Admin" (
"id" TEXT NOT NULL,
"fullName" TEXT NOT NULL,
"firstName" TEXT,
"lastName" TEXT,
"mobile" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"role" "AdminRole" NOT NULL,
"userTypeId" INTEGER,
"branchId" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Admin_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "Admin_mobile_key" ON "Admin" ("mobile");
CREATE INDEX IF NOT EXISTS "Admin_role_idx" ON "Admin" ("role");
CREATE INDEX IF NOT EXISTS "Admin_userTypeId_idx" ON "Admin" ("userTypeId");
CREATE INDEX IF NOT EXISTS "Admin_firstName_idx" ON "Admin" ("firstName");
CREATE INDEX IF NOT EXISTS "Admin_lastName_idx" ON "Admin" ("lastName");
DO $$ BEGIN
ALTER TABLE "Admin" ADD CONSTRAINT "Admin_userTypeId_fkey"
FOREIGN KEY ("userTypeId") REFERENCES "user_type"("id") ON DELETE SET NULL ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE "Admin" ADD CONSTRAINT "Admin_branchId_fkey"
FOREIGN KEY ("branchId") REFERENCES "Branch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- ── 2. Copy the staff (everyone who is not a sales expert), ids preserved ──
--
-- Done BEFORE the name-sync trigger exists, so the stored names are copied
-- exactly as they are rather than rebuilt.
INSERT INTO "Admin"
("id", "fullName", "firstName", "lastName", "mobile", "passwordHash",
"role", "userTypeId", "branchId", "isActive", "createdAt")
SELECT
"id", "fullName", "firstName", "lastName", "mobile", "passwordHash",
"role", "userTypeId", "branchId", "isActive", "createdAt"
FROM "AdminUser"
WHERE "role" <> 'SALES_EXPERT'
ON CONFLICT ("id") DO NOTHING;
-- Same two-way fullName <-> firstName/lastName sync as AdminUser, sharing its
-- function (created in 20260922180000_sales_expert_profile_and_admin_name_parts).
DROP TRIGGER IF EXISTS admin_sync_full_name_trg ON "Admin";
CREATE TRIGGER admin_sync_full_name_trg
BEFORE INSERT OR UPDATE ON "Admin"
FOR EACH ROW EXECUTE FUNCTION admin_user_sync_full_name();
-- ── 3. Re-point the foreign keys that mean "an admin did this" ─────────────
--
-- Same constraint names, same column names, same ON DELETE / ON UPDATE actions;
-- only the referenced table changes. "Order"."salesAgentId",
-- "Customer"."referredByAgentId" and "SalesExpertProfile"."adminUserId" are
-- about SALES EXPERTS and keep pointing at "AdminUser".
ALTER TABLE "Workshop" DROP CONSTRAINT IF EXISTS "Workshop_supervisorId_fkey";
ALTER TABLE "Workshop" ADD CONSTRAINT "Workshop_supervisorId_fkey"
FOREIGN KEY ("supervisorId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
ALTER TABLE "OrderStatusEvent" DROP CONSTRAINT IF EXISTS "OrderStatusEvent_actorAdminUserId_fkey";
ALTER TABLE "OrderStatusEvent" ADD CONSTRAINT "OrderStatusEvent_actorAdminUserId_fkey"
FOREIGN KEY ("actorAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
ALTER TABLE "PreOrderStatusEvent" DROP CONSTRAINT IF EXISTS "PreOrderStatusEvent_actorAdminUserId_fkey";
ALTER TABLE "PreOrderStatusEvent" ADD CONSTRAINT "PreOrderStatusEvent_actorAdminUserId_fkey"
FOREIGN KEY ("actorAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
ALTER TABLE "LedgerEntry" DROP CONSTRAINT IF EXISTS "LedgerEntry_postedByAdminUserId_fkey";
ALTER TABLE "LedgerEntry" ADD CONSTRAINT "LedgerEntry_postedByAdminUserId_fkey"
FOREIGN KEY ("postedByAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
ALTER TABLE "BranchAllocation" DROP CONSTRAINT IF EXISTS "BranchAllocation_allocatorAdminUserId_fkey";
ALTER TABLE "BranchAllocation" ADD CONSTRAINT "BranchAllocation_allocatorAdminUserId_fkey"
FOREIGN KEY ("allocatorAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
ALTER TABLE "SmsMessage" DROP CONSTRAINT IF EXISTS "SmsMessage_sentByAdminUserId_fkey";
ALTER TABLE "SmsMessage" ADD CONSTRAINT "SmsMessage_sentByAdminUserId_fkey"
FOREIGN KEY ("sentByAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
ALTER TABLE "Notification" DROP CONSTRAINT IF EXISTS "Notification_sentByAdminUserId_fkey";
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_sentByAdminUserId_fkey"
FOREIGN KEY ("sentByAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
ALTER TABLE "UploadedFile" DROP CONSTRAINT IF EXISTS "UploadedFile_uploadedByAdminUserId_fkey";
ALTER TABLE "UploadedFile" ADD CONSTRAINT "UploadedFile_uploadedByAdminUserId_fkey"
FOREIGN KEY ("uploadedByAdminUserId") REFERENCES "Admin"("id") ON DELETE SET NULL ON UPDATE CASCADE NOT VALID;
-- Panel tokens are issued to staff only. CASCADE as before: deleting an admin
-- ends their sessions.
ALTER TABLE "AdminSession" DROP CONSTRAINT IF EXISTS "AdminSession_adminUserId_fkey";
ALTER TABLE "AdminSession" ADD CONSTRAINT "AdminSession_adminUserId_fkey"
FOREIGN KEY ("adminUserId") REFERENCES "Admin"("id") ON DELETE CASCADE ON UPDATE CASCADE NOT VALID;
-- ── 4. Validate; never fail the deploy over an old orphan ──────────────────
DO $$
DECLARE
c record;
BEGIN
FOR c IN
SELECT * FROM (VALUES
('Workshop', 'Workshop_supervisorId_fkey'),
('OrderStatusEvent', 'OrderStatusEvent_actorAdminUserId_fkey'),
('PreOrderStatusEvent', 'PreOrderStatusEvent_actorAdminUserId_fkey'),
('LedgerEntry', 'LedgerEntry_postedByAdminUserId_fkey'),
('BranchAllocation', 'BranchAllocation_allocatorAdminUserId_fkey'),
('SmsMessage', 'SmsMessage_sentByAdminUserId_fkey'),
('Notification', 'Notification_sentByAdminUserId_fkey'),
('UploadedFile', 'UploadedFile_uploadedByAdminUserId_fkey'),
('AdminSession', 'AdminSession_adminUserId_fkey')
) AS t(tbl, con)
LOOP
BEGIN
EXECUTE format('ALTER TABLE %I VALIDATE CONSTRAINT %I', c.tbl, c.con);
EXCEPTION WHEN foreign_key_violation THEN
RAISE NOTICE '% on % left NOT VALID: some existing rows reference an id that is not in "Admin" (see docs/ADMIN-TABLE-SPLIT.md)', c.con, c.tbl;
END;
END LOOP;
END $$;
+4 -4
View File
@@ -1846,7 +1846,7 @@ async function main() {
update: {},
create: { id: "workshop-central", name: "\u06A9\u0627\u0631\u06AF\u0627\u0647 \u0645\u0631\u06A9\u0632\u06CC \u0633\u0627\u062E\u062A \u0637\u0644\u0627" }
});
await prisma.adminUser.upsert({
await prisma.admin.upsert({
where: { mobile: DEV_TEST_PHONE },
update: { passwordHash: DEV_TEST_PHONE_PASSWORD_HASH, userTypeId: 85, branchId: null },
create: {
@@ -1858,7 +1858,7 @@ async function main() {
branchId: null
}
});
await prisma.adminUser.upsert({
await prisma.admin.upsert({
where: { mobile: "09120000001" },
update: { passwordHash: PLACEHOLDER_PASSWORD_HASH, userTypeId: 85, branchId: null },
create: {
@@ -1899,7 +1899,7 @@ async function main() {
wagePercent: 1.2
}
});
await prisma.adminUser.upsert({
await prisma.admin.upsert({
where: { mobile: "09123334444" },
update: { passwordHash: PLACEHOLDER_PASSWORD_HASH, userTypeId: 88 },
create: {
@@ -1910,7 +1910,7 @@ async function main() {
userTypeId: 88
}
});
await prisma.adminUser.upsert({
await prisma.admin.upsert({
where: { mobile: "09125556666" },
update: { passwordHash: PLACEHOLDER_PASSWORD_HASH, userTypeId: 87, branchId: null },
create: {
File diff suppressed because one or more lines are too long
+23 -8
View File
@@ -130,6 +130,20 @@ exports.Prisma.CityScalarFieldEnum = {
percentAux: 'percentAux'
};
exports.Prisma.AdminScalarFieldEnum = {
id: 'id',
fullName: 'fullName',
firstName: 'firstName',
lastName: 'lastName',
mobile: 'mobile',
passwordHash: 'passwordHash',
role: 'role',
userTypeId: 'userTypeId',
branchId: 'branchId',
isActive: 'isActive',
createdAt: 'createdAt'
};
exports.Prisma.AdminUserScalarFieldEnum = {
id: 'id',
fullName: 'fullName',
@@ -170,7 +184,7 @@ exports.Prisma.AppSessionScalarFieldEnum = {
exports.Prisma.AdminSessionScalarFieldEnum = {
id: 'id',
jti: 'jti',
adminUserId: 'adminUserId',
adminId: 'adminId',
iamSessionId: 'iamSessionId',
iamSub: 'iamSub',
ipAddress: 'ipAddress',
@@ -370,7 +384,7 @@ exports.Prisma.OrderStatusEventScalarFieldEnum = {
fromStatus: 'fromStatus',
toStatus: 'toStatus',
note: 'note',
actorAdminUserId: 'actorAdminUserId',
actorAdminId: 'actorAdminId',
createdAt: 'createdAt'
};
@@ -397,7 +411,7 @@ exports.Prisma.PreOrderStatusEventScalarFieldEnum = {
id: 'id',
preOrderId: 'preOrderId',
changes: 'changes',
actorAdminUserId: 'actorAdminUserId',
actorAdminId: 'actorAdminId',
ipAddress: 'ipAddress',
createdAt: 'createdAt'
};
@@ -414,7 +428,7 @@ exports.Prisma.LedgerEntryScalarFieldEnum = {
orderId: 'orderId',
confirmed: 'confirmed',
confirmedAt: 'confirmedAt',
postedByAdminUserId: 'postedByAdminUserId',
postedByAdminId: 'postedByAdminId',
labName: 'labName',
hallmark: 'hallmark',
accountName: 'accountName',
@@ -549,7 +563,7 @@ exports.Prisma.PromotionExclusionScalarFieldEnum = {
exports.Prisma.BranchAllocationScalarFieldEnum = {
id: 'id',
branchId: 'branchId',
allocatorAdminUserId: 'allocatorAdminUserId',
allocatorAdminId: 'allocatorAdminId',
note: 'note',
createdAt: 'createdAt'
};
@@ -567,7 +581,7 @@ exports.Prisma.SmsMessageScalarFieldEnum = {
recipients: 'recipients',
body: 'body',
sentCount: 'sentCount',
sentByAdminUserId: 'sentByAdminUserId',
sentByAdminId: 'sentByAdminId',
createdAt: 'createdAt'
};
@@ -576,7 +590,7 @@ exports.Prisma.NotificationScalarFieldEnum = {
title: 'title',
body: 'body',
audience: 'audience',
sentByAdminUserId: 'sentByAdminUserId',
sentByAdminId: 'sentByAdminId',
createdAt: 'createdAt'
};
@@ -609,7 +623,7 @@ exports.Prisma.UploadedFileScalarFieldEnum = {
mimeType: 'mimeType',
sizeBytes: 'sizeBytes',
purpose: 'purpose',
uploadedByAdminUserId: 'uploadedByAdminUserId',
uploadedByAdminId: 'uploadedByAdminId',
createdAt: 'createdAt'
};
@@ -782,6 +796,7 @@ exports.MessageAudience = exports.$Enums.MessageAudience = {
exports.Prisma.ModelName = {
City: 'City',
Admin: 'Admin',
AdminUser: 'AdminUser',
AppSession: 'AppSession',
AdminSession: 'AdminSession',
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "prisma-client-8945b25ad1891f9eaff8d38ca7f3c1b19a3f2d11c0c8521549c219186ff7566b",
"name": "prisma-client-d98bddf3599c950eb3fcd511efa7f226c4ef4c5c3cbc261683fb1118a763a924",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",
+92 -48
View File
@@ -140,6 +140,61 @@ model City {
@@index([parentId])
}
/// A staff account for the ADMIN PANEL — and nothing else.
///
/// Split out of AdminUser (which now holds sales experts only) so that:
/// * the same mobile can be an admin AND a sales expert — they are two
/// separate roles, each unique only within its own table;
/// * the expert table (production: ~12,000 rows) is never written by panel
/// staff management.
/// Migration 20261026120000_split_admin_table copies the existing non-expert
/// AdminUser rows here WITH THEIR IDS, so issued panel tokens and every
/// "who did this" column keep resolving. The old rows are left in AdminUser,
/// dormant; no code reads them as admins any more.
///
/// ⚠️ fullName/firstName/lastName are kept in sync by the DB trigger
/// `admin_sync_full_name_trg` (it shares the function admin_user_sync_full_name) — same semantics as AdminUser: the parts win.
model Admin {
id String @id @default(cuid())
fullName String
firstName String?
lastName String?
mobile String @unique
passwordHash String
/// Never SALES_EXPERT — that role lives in AdminUser. Enforced in
/// AdminUsersService, not in the database, so the enum stays shared.
role AdminRole
userTypeId Int?
userType UserType? @relation(fields: [userTypeId], references: [id])
branchId String?
branch Branch? @relation(fields: [branchId], references: [id])
isActive Boolean @default(true)
createdAt DateTime @default(now())
supervisedWorkshops Workshop[] @relation("WorkshopSupervisor")
branchAllocations BranchAllocation[] @relation("AllocationAuthor")
postedLedgerEntries LedgerEntry[]
orderStatusEvents OrderStatusEvent[]
preOrderStatusEvents PreOrderStatusEvent[]
sentSmsMessages SmsMessage[] @relation("SmsAuthor")
sentNotifications Notification[] @relation("NotificationAuthor")
uploadedFiles UploadedFile[] @relation("FileUploader")
/// Issued admin-panel tokens, recorded only while session tracking is on
/// (AUTH_SESSION_TRACKING, or implicitly whenever the IAM is enabled) so a
/// token can be revoked before it expires. See AdminSession.
sessions AdminSession[]
@@index([role])
@@index([userTypeId])
@@index([firstName])
@@index([lastName])
}
/// A SALES EXPERT (role SALES_EXPERT). Panel staff live in `Admin`; rows here with
/// any other role are pre-split leftovers and are ignored by the code.
model AdminUser {
id String @id @default(cuid())
@@ -185,26 +240,13 @@ model AdminUser {
pasazhName String? // legacy pasazh_name
memberCode String? // legacy mcode
referredCustomers Customer[] @relation("ReferralAgent")
referredCustomers Customer[] @relation("ReferralAgent")
/// Orders credited to this sales expert — see Order.salesAgentId.
creditedOrders Order[] @relation("OrderSalesAgent")
supervisedWorkshops Workshop[] @relation("WorkshopSupervisor")
branchAllocations BranchAllocation[] @relation("AllocationAuthor")
postedLedgerEntries LedgerEntry[]
orderStatusEvents OrderStatusEvent[]
preOrderStatusEvents PreOrderStatusEvent[]
sentSmsMessages SmsMessage[] @relation("SmsAuthor")
sentNotifications Notification[] @relation("NotificationAuthor")
uploadedFiles UploadedFile[] @relation("FileUploader")
creditedOrders Order[] @relation("OrderSalesAgent")
/// Only ever set for role = SALES_EXPERT (D17).
salesExpertProfile SalesExpertProfile?
/// Issued admin-panel tokens, recorded only while session tracking is on
/// (AUTH_SESSION_TRACKING, or implicitly whenever the IAM is enabled) so a
/// token can be revoked before it expires. See AdminSession.
sessions AdminSession[]
@@index([role])
@@index([userTypeId])
@@index([avatarFileId])
@@ -274,8 +316,8 @@ model AdminSession {
/// The token's `jti` claim. Unique: one row per issued token.
jti String @unique
adminUserId String
adminUser AdminUser @relation(fields: [adminUserId], references: [id], onDelete: Cascade)
adminId String @map("adminUserId")
admin Admin @relation(fields: [adminId], references: [id], onDelete: Cascade)
/// Set when the login was verified by the IAM: its session id (`sid`) and the
/// stable user id (`sub`) there. Back-channel logout arrives keyed by these —
@@ -294,7 +336,7 @@ model AdminSession {
revokedAt DateTime?
revokedReason String?
@@index([adminUserId])
@@index([adminId], map: "AdminSession_adminUserId_idx")
@@index([iamSessionId])
@@index([iamSub])
@@index([expiresAt])
@@ -571,6 +613,7 @@ model Branch {
cityId String?
city City? @relation(fields: [cityId], references: [id])
adminUsers AdminUser[]
admins Admin[]
orders Order[]
allocations BranchAllocation[]
}
@@ -583,7 +626,7 @@ model Workshop {
phone String?
/// The supervisor created alongside the workshop by the legacy Add form.
supervisorId String?
supervisor AdminUser? @relation("WorkshopSupervisor", fields: [supervisorId], references: [id])
supervisor Admin? @relation("WorkshopSupervisor", fields: [supervisorId], references: [id])
isActive Boolean @default(true)
preOrders PreOrder[]
}
@@ -826,15 +869,15 @@ model OrderItem {
}
model OrderStatusEvent {
id String @id @default(cuid())
orderId String
order Order @relation(fields: [orderId], references: [id])
fromStatus OrderStatus?
toStatus OrderStatus
note String?
actorAdminUserId String?
actorAdminUser AdminUser? @relation(fields: [actorAdminUserId], references: [id])
createdAt DateTime @default(now())
id String @id @default(cuid())
orderId String
order Order @relation(fields: [orderId], references: [id])
fromStatus OrderStatus?
toStatus OrderStatus
note String?
actorAdminId String? @map("actorAdminUserId")
actorAdmin Admin? @relation(fields: [actorAdminId], references: [id])
createdAt DateTime @default(now())
@@index([orderId])
}
@@ -875,15 +918,15 @@ model PreOrder {
}
model PreOrderStatusEvent {
id String @id @default(cuid())
preOrderId String
preOrder PreOrder @relation(fields: [preOrderId], references: [id])
id String @id @default(cuid())
preOrderId String
preOrder PreOrder @relation(fields: [preOrderId], references: [id])
/// Field-level diff, e.g. {"status": {"old": "PLACED", "new": "ADMIN_APPROVED"}}
changes Json
actorAdminUserId String?
actorAdminUser AdminUser? @relation(fields: [actorAdminUserId], references: [id])
ipAddress String?
createdAt DateTime @default(now())
changes Json
actorAdminId String? @map("actorAdminUserId")
actorAdmin Admin? @relation(fields: [actorAdminId], references: [id])
ipAddress String?
createdAt DateTime @default(now())
@@index([preOrderId])
}
@@ -912,10 +955,10 @@ model LedgerEntry {
order Order? @relation(fields: [orderId], references: [id])
/// Pending entries don't count toward the cached customer balance until confirmed.
confirmed Boolean @default(false)
confirmedAt DateTime?
postedByAdminUserId String?
postedByAdminUser AdminUser? @relation(fields: [postedByAdminUserId], references: [id])
confirmed Boolean @default(false)
confirmedAt DateTime?
postedByAdminId String? @map("postedByAdminUserId")
postedByAdmin Admin? @relation(fields: [postedByAdminId], references: [id])
/// Instrument metadata from the legacy "cash" document form. Which fields are
/// filled depends on sourceType: a melted-gold receipt carries lab/hallmark and
@@ -997,6 +1040,7 @@ model UserType {
permissions Permission[]
adminUsers AdminUser[]
admins Admin[]
@@map("user_type")
}
@@ -1251,8 +1295,8 @@ model BranchAllocation {
branch Branch @relation(fields: [branchId], references: [id])
/// Legacy `allocator_user` — the admin who filed the allocation.
allocatorAdminUserId String?
allocatorAdminUser AdminUser? @relation("AllocationAuthor", fields: [allocatorAdminUserId], references: [id])
allocatorAdminId String? @map("allocatorAdminUserId")
allocatorAdmin Admin? @relation("AllocationAuthor", fields: [allocatorAdminId], references: [id])
note String?
createdAt DateTime @default(now())
@@ -1299,8 +1343,8 @@ model SmsMessage {
/// How many numbers the send actually resolved to.
sentCount Int @default(0)
sentByAdminUserId String?
sentByAdminUser AdminUser? @relation("SmsAuthor", fields: [sentByAdminUserId], references: [id])
sentByAdminId String? @map("sentByAdminUserId")
sentByAdmin Admin? @relation("SmsAuthor", fields: [sentByAdminId], references: [id])
createdAt DateTime @default(now())
@@ -1313,8 +1357,8 @@ model Notification {
body String
audience MessageAudience @default(ALL_CUSTOMERS)
sentByAdminUserId String?
sentByAdminUser AdminUser? @relation("NotificationAuthor", fields: [sentByAdminUserId], references: [id])
sentByAdminId String? @map("sentByAdminUserId")
sentByAdmin Admin? @relation("NotificationAuthor", fields: [sentByAdminId], references: [id])
createdAt DateTime @default(now())
@@ -1379,8 +1423,8 @@ model UploadedFile {
/// 'customer.nationalId'. Kept loose so a new form needs no schema change.
purpose String?
uploadedByAdminUserId String?
uploadedByAdminUser AdminUser? @relation("FileUploader", fields: [uploadedByAdminUserId], references: [id])
uploadedByAdminId String? @map("uploadedByAdminUserId")
uploadedByAdmin Admin? @relation("FileUploader", fields: [uploadedByAdminId], references: [id])
createdAt DateTime @default(now())
customerAvatars Customer[] @relation("CustomerAvatar")
+23 -8
View File
@@ -130,6 +130,20 @@ exports.Prisma.CityScalarFieldEnum = {
percentAux: 'percentAux'
};
exports.Prisma.AdminScalarFieldEnum = {
id: 'id',
fullName: 'fullName',
firstName: 'firstName',
lastName: 'lastName',
mobile: 'mobile',
passwordHash: 'passwordHash',
role: 'role',
userTypeId: 'userTypeId',
branchId: 'branchId',
isActive: 'isActive',
createdAt: 'createdAt'
};
exports.Prisma.AdminUserScalarFieldEnum = {
id: 'id',
fullName: 'fullName',
@@ -170,7 +184,7 @@ exports.Prisma.AppSessionScalarFieldEnum = {
exports.Prisma.AdminSessionScalarFieldEnum = {
id: 'id',
jti: 'jti',
adminUserId: 'adminUserId',
adminId: 'adminId',
iamSessionId: 'iamSessionId',
iamSub: 'iamSub',
ipAddress: 'ipAddress',
@@ -370,7 +384,7 @@ exports.Prisma.OrderStatusEventScalarFieldEnum = {
fromStatus: 'fromStatus',
toStatus: 'toStatus',
note: 'note',
actorAdminUserId: 'actorAdminUserId',
actorAdminId: 'actorAdminId',
createdAt: 'createdAt'
};
@@ -397,7 +411,7 @@ exports.Prisma.PreOrderStatusEventScalarFieldEnum = {
id: 'id',
preOrderId: 'preOrderId',
changes: 'changes',
actorAdminUserId: 'actorAdminUserId',
actorAdminId: 'actorAdminId',
ipAddress: 'ipAddress',
createdAt: 'createdAt'
};
@@ -414,7 +428,7 @@ exports.Prisma.LedgerEntryScalarFieldEnum = {
orderId: 'orderId',
confirmed: 'confirmed',
confirmedAt: 'confirmedAt',
postedByAdminUserId: 'postedByAdminUserId',
postedByAdminId: 'postedByAdminId',
labName: 'labName',
hallmark: 'hallmark',
accountName: 'accountName',
@@ -549,7 +563,7 @@ exports.Prisma.PromotionExclusionScalarFieldEnum = {
exports.Prisma.BranchAllocationScalarFieldEnum = {
id: 'id',
branchId: 'branchId',
allocatorAdminUserId: 'allocatorAdminUserId',
allocatorAdminId: 'allocatorAdminId',
note: 'note',
createdAt: 'createdAt'
};
@@ -567,7 +581,7 @@ exports.Prisma.SmsMessageScalarFieldEnum = {
recipients: 'recipients',
body: 'body',
sentCount: 'sentCount',
sentByAdminUserId: 'sentByAdminUserId',
sentByAdminId: 'sentByAdminId',
createdAt: 'createdAt'
};
@@ -576,7 +590,7 @@ exports.Prisma.NotificationScalarFieldEnum = {
title: 'title',
body: 'body',
audience: 'audience',
sentByAdminUserId: 'sentByAdminUserId',
sentByAdminId: 'sentByAdminId',
createdAt: 'createdAt'
};
@@ -609,7 +623,7 @@ exports.Prisma.UploadedFileScalarFieldEnum = {
mimeType: 'mimeType',
sizeBytes: 'sizeBytes',
purpose: 'purpose',
uploadedByAdminUserId: 'uploadedByAdminUserId',
uploadedByAdminId: 'uploadedByAdminId',
createdAt: 'createdAt'
};
@@ -782,6 +796,7 @@ exports.MessageAudience = exports.$Enums.MessageAudience = {
exports.Prisma.ModelName = {
City: 'City',
Admin: 'Admin',
AdminUser: 'AdminUser',
AppSession: 'AppSession',
AdminSession: 'AdminSession',