Files
tehran_tala/apps/api/src/modules/admin-branches/admin-branches.service.ts
T
2026-10-07 20:20:27 +03:30

363 lines
13 KiB
TypeScript

import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { AdminRole, Prisma } from '@prisma/client';
import * as bcrypt from 'bcryptjs';
import { PrismaService } from '../../prisma/prisma.service';
import { TableService } from '../admin-table/table.service';
import { NO_PASSWORD_YET_HASH } from '../auth/password-hash';
import type { TableQueryDto } from '../admin-table/table-query.dto';
/**
* Branches and workshops — legacy views/branch/* and views/workshop/*, served by
* BranchController and WorkshopController:
* POST branch/table | workshop/table -> the grids
* POST branch | workshop -> create
* GET branch/{id} | workshop/{id} -> one row
* PUT branch/{id} | workshop/{id} -> update
* GET branch/active/{id} | workshop/active/{id} -> toggle active
*
* The two screens differ in how their manager is handled, and that difference is
* preserved: a BRANCH picks an existing admin (`user_id`), while a WORKSHOP
* creates its supervisor inline from three fields (user_fname/lname/phone).
*/
@Injectable()
export class AdminBranchesService {
constructor(
private readonly prisma: PrismaService,
private readonly table: TableService,
) {}
// ── Branches ───────────────────────────────────────────────────────
/** Legacy `POST branch/table`. */
async listBranches(query: TableQueryDto) {
this.table.assertNotExport(query);
const aliases: Record<string, string> = { 'user.name': 'adminUsers.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'],
defaultOrderBy: { name: 'asc' },
});
void aliases;
const [rows, total] = await Promise.all([
this.prisma.branch.findMany({
where: where as Prisma.BranchWhereInput,
orderBy: orderBy as Prisma.BranchOrderByWithRelationInput,
skip,
take,
include: {
city: true,
adminUsers: { where: { isActive: true }, take: 1 },
_count: { select: { adminUsers: true, orders: true } },
},
}),
this.prisma.branch.count({ where: where as Prisma.BranchWhereInput }),
]);
return this.table.respond(
rows.map((b) => ({
id: b.id,
name: b.name,
address: b.address,
phone: b.phone,
active: b.isActive ? 1 : 0,
is_active: b.isActive,
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,
orders_count: b._count.orders,
})),
total,
query,
page,
limit,
);
}
async findBranch(id: string) {
const b = await this.prisma.branch.findUnique({
where: { id },
include: { city: true, adminUsers: { where: { isActive: true }, take: 1 } },
});
if (!b) throw new NotFoundException('شعبه یافت نشد');
return {
id: b.id,
name: b.name,
address: b.address,
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,
};
}
/** Legacy `POST branch` — the manager is an EXISTING admin picked by id. */
async createBranch(data: Record<string, unknown>) {
const name = String(data.name ?? '').trim();
if (!name) throw new BadRequestException('نام شعبه الزامی است');
const branch = await this.prisma.branch.create({
data: {
name,
address: (data.address as string) || null,
phone: (data.phone as string) || null,
cityId: (data.city_id as string) || null,
},
});
if (data.user_id) {
await this.assignBranchManager(branch.id, String(data.user_id));
}
return this.findBranch(branch.id);
}
/** Legacy `PUT branch/{id}`. */
async updateBranch(id: string, data: Record<string, unknown>) {
const existing = await this.prisma.branch.findUnique({ where: { id } });
if (!existing) throw new NotFoundException('شعبه یافت نشد');
const patch: Prisma.BranchUpdateInput = {};
if (data.name !== undefined) patch.name = String(data.name);
if (data.address !== undefined) patch.address = (data.address as string) || null;
if (data.phone !== undefined) patch.phone = (data.phone as string) || null;
if (data.city_id !== undefined) {
patch.city = data.city_id ? { connect: { id: String(data.city_id) } } : { disconnect: true };
}
await this.prisma.branch.update({ where: { id }, data: patch });
if (data.user_id !== undefined) {
await this.assignBranchManager(id, (data.user_id as string) || null);
}
return this.findBranch(id);
}
/**
* Moves the named admin onto this branch and clears whoever was on it before,
* so a branch keeps exactly one manager. Legacy did the same through its
* `->update(['branch_id' => null])` / `->update(['branch_id' => $id])` pair.
*/
private async assignBranchManager(branchId: string, userId: string | null) {
await this.prisma.$transaction(async (tx) => {
await tx.adminUser.updateMany({ where: { branchId }, data: { branchId: null } });
if (userId) {
const user = await tx.adminUser.findUnique({ where: { id: userId } });
if (!user) throw new NotFoundException('کاربر انتخاب‌شده یافت نشد');
await tx.adminUser.update({ where: { id: userId }, data: { branchId } });
}
});
}
/** Legacy `GET branch/active/{id}` — a toggle. */
async toggleBranchActive(id: string) {
const b = await this.prisma.branch.findUnique({ where: { id } });
if (!b) throw new NotFoundException('شعبه یافت نشد');
return this.prisma.branch.update({
where: { id },
data: { isActive: !b.isActive },
});
}
// ── Workshops ──────────────────────────────────────────────────────
/** Legacy `POST workshop/table`. */
async listWorkshops(query: TableQueryDto) {
this.table.assertNotExport(query);
const { where, orderBy, skip, take, page, limit } = this.table.build(query, {
searchable: ['name', 'address', 'phone'],
allowed: ['id', 'name', 'address', 'phone', 'isActive'],
defaultOrderBy: { name: 'asc' },
});
const [rows, total] = await Promise.all([
this.prisma.workshop.findMany({
where: where as Prisma.WorkshopWhereInput,
orderBy: orderBy as Prisma.WorkshopOrderByWithRelationInput,
skip,
take,
include: { supervisor: true, _count: { select: { preOrders: true } } },
}),
this.prisma.workshop.count({ where: where as Prisma.WorkshopWhereInput }),
]);
return this.table.respond(
rows.map((w) => this.workshopRow(w)),
total,
query,
page,
limit,
);
}
private workshopRow(
w: Prisma.WorkshopGetPayload<{
include: { supervisor: true; _count: { select: { preOrders: true } } };
}>,
) {
return {
id: w.id,
name: w.name,
address: w.address,
phone: w.phone,
active: w.isActive ? 1 : 0,
is_active: w.isActive,
user: w.supervisor ? { id: w.supervisor.id, name: w.supervisor.fullName } : null,
user_id: w.supervisorId,
preorders_count: w._count.preOrders,
};
}
async findWorkshop(id: string) {
const w = await this.prisma.workshop.findUnique({
where: { id },
include: { supervisor: true, _count: { select: { preOrders: true } } },
});
if (!w) throw new NotFoundException('کارگاه یافت نشد');
const row = this.workshopRow(w);
// The edit form splits the supervisor back into its three fields.
const parts = String(w.supervisor?.fullName ?? '').trim().split(' ');
return {
...row,
workshop_name: w.name,
workshop_address: w.address,
workshop_phone: w.phone,
user_fname: parts[0] ?? '',
user_lname: parts.slice(1).join(' '),
user_phone: w.supervisor?.mobile ?? '',
};
}
/**
* Legacy `POST workshop` — creates the workshop AND its supervisor together,
* from user_fname / user_lname / user_phone.
*/
async createWorkshop(data: Record<string, unknown>) {
const name = String(data.workshop_name ?? data.name ?? '').trim();
if (!name) throw new BadRequestException('نام کارگاه الزامی است');
const supervisorId = await this.upsertSupervisor(data, null);
const w = await this.prisma.workshop.create({
data: {
name,
address: (data.workshop_address as string) || (data.address as string) || null,
phone: (data.workshop_phone as string) || (data.phone as string) || null,
supervisorId,
},
});
return this.findWorkshop(w.id);
}
/** Legacy `PUT workshop/{id}`. */
async updateWorkshop(id: string, data: Record<string, unknown>) {
const existing = await this.prisma.workshop.findUnique({ where: { id } });
if (!existing) throw new NotFoundException('کارگاه یافت نشد');
const patch: Prisma.WorkshopUpdateInput = {};
if (data.workshop_name !== undefined || data.name !== undefined) {
patch.name = String(data.workshop_name ?? data.name);
}
if (data.workshop_address !== undefined || data.address !== undefined) {
patch.address = ((data.workshop_address ?? data.address) as string) || null;
}
if (data.workshop_phone !== undefined || data.phone !== undefined) {
patch.phone = ((data.workshop_phone ?? data.phone) as string) || null;
}
const supervisorId = await this.upsertSupervisor(data, existing.supervisorId);
if (supervisorId !== undefined) {
patch.supervisor = supervisorId
? { connect: { id: supervisorId } }
: { disconnect: true };
}
await this.prisma.workshop.update({ where: { id }, data: patch });
return this.findWorkshop(id);
}
/**
* Creates or updates the workshop's supervisor from the inline fields.
* Returns the id to link, or `undefined` when the form sent no supervisor data
* at all (so an update leaves the existing link alone).
*/
private async upsertSupervisor(
data: Record<string, unknown>,
currentId: string | null,
): Promise<string | null | undefined> {
const fname = String(data.user_fname ?? '').trim();
const lname = String(data.user_lname ?? '').trim();
const phone = String(data.user_phone ?? '').trim();
if (!fname && !lname && !phone) return undefined;
if (!phone) throw new BadRequestException('موبایل سرپرست کارگاه الزامی است');
const fullName = `${fname} ${lname}`.trim();
if (currentId) {
const current = await this.prisma.adminUser.findUnique({ where: { id: currentId } });
if (current) {
if (phone !== current.mobile) {
const clash = await this.prisma.adminUser.findUnique({ where: { mobile: phone } });
if (clash && clash.id !== currentId) {
throw new BadRequestException('این شماره موبایل قبلاً ثبت شده است');
}
}
await this.prisma.adminUser.update({
where: { id: currentId },
data: { fullName, mobile: phone },
});
return currentId;
}
}
// Reuse an existing admin with that mobile rather than colliding on the
// unique constraint.
const existing = await this.prisma.adminUser.findUnique({ where: { mobile: phone } });
if (existing) return existing.id;
const created = await this.prisma.adminUser.create({
data: {
fullName,
mobile: phone,
// No password is set: the supervisor cannot sign in until one is issued.
passwordHash: NO_PASSWORD_YET_HASH,
role: AdminRole.WORKSHOP_SUPERVISOR,
},
});
return created.id;
}
/** Legacy `GET workshop/active/{id}` — a toggle. */
async toggleWorkshopActive(id: string) {
const w = await this.prisma.workshop.findUnique({ where: { id } });
if (!w) throw new NotFoundException('کارگاه یافت نشد');
return this.prisma.workshop.update({
where: { id },
data: { isActive: !w.isActive },
});
}
/** The admin picker the branch form uses — legacy `GET user/list`. */
async userList() {
const rows = await this.prisma.adminUser.findMany({
where: { isActive: true },
orderBy: { fullName: 'asc' },
select: { id: true, fullName: true, mobile: true, branchId: true },
});
return rows.map((u) => ({
id: u.id,
name: u.fullName,
mobile: u.mobile,
branch_id: u.branchId,
}));
}
}