363 lines
13 KiB
TypeScript
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,
|
|
}));
|
|
}
|
|
}
|