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 = { '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) { 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) { 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) { 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) { 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, currentId: string | null, ): Promise { 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, })); } }