feat: complete admin panel and backend admin api migration
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, Post, Put } from '@nestjs/common';
|
||||
import { AdminInvoicesService } from './admin-invoices.service';
|
||||
import { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
import { CurrentAdmin, RequirePermission } from '../auth/guards';
|
||||
import type { AuthenticatedAdmin } from '../auth/jwt.strategy';
|
||||
|
||||
/**
|
||||
* Legacy endpoints (BranchController's `branch/product` family):
|
||||
* POST branch/product/table
|
||||
* POST branch/product
|
||||
* GET branch/product/{id}
|
||||
* DELETE branch/product/{id}
|
||||
* GET branch/list -> the branch picker
|
||||
*/
|
||||
@Controller('admin/invoices')
|
||||
export class AdminInvoicesController {
|
||||
constructor(private readonly invoices: AdminInvoicesService) {}
|
||||
|
||||
@Post('list')
|
||||
@HttpCode(200)
|
||||
@RequirePermission('invoice.list')
|
||||
list(@Body() query: TableQueryDto) {
|
||||
return this.invoices.list(query);
|
||||
}
|
||||
|
||||
@Get('branch-list')
|
||||
@RequirePermission('invoice.create')
|
||||
branchList() {
|
||||
return this.invoices.branchList();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HttpCode(200)
|
||||
@RequirePermission('invoice.create')
|
||||
create(
|
||||
@Body() data: { branch_id?: string; products?: string[]; note?: string },
|
||||
@CurrentAdmin() admin: AuthenticatedAdmin,
|
||||
) {
|
||||
return this.invoices.create(data, admin.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermission('invoice.list')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.invoices.findOne(id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@RequirePermission('invoice.list')
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() data: { branch_id?: string; products?: string[]; note?: string },
|
||||
) {
|
||||
return this.invoices.update(id, data);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission('invoice.list')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.invoices.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from '../../prisma/prisma.module';
|
||||
import { AdminInvoicesController } from './admin-invoices.controller';
|
||||
import { AdminInvoicesService } from './admin-invoices.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [AdminInvoicesController],
|
||||
providers: [AdminInvoicesService],
|
||||
})
|
||||
export class AdminInvoicesModule {}
|
||||
@@ -0,0 +1,245 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { TableService } from '../admin-table/table.service';
|
||||
import type { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
|
||||
/**
|
||||
* Branch stock allocations — legacy views/invoice/* ("فاکتور انتقال"), served by
|
||||
* BranchController as `branch/product`:
|
||||
* POST branch/product/table -> the grid
|
||||
* POST branch/product -> create { branch_id, products: [id...] }
|
||||
* GET branch/product/{id} -> one allocation
|
||||
* DELETE branch/product/{id} -> delete
|
||||
*
|
||||
* Despite the screen being labelled an invoice, it is not a sales document: it
|
||||
* records which products head office allocated to a branch, and who did it.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AdminInvoicesService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly table: TableService,
|
||||
) {}
|
||||
|
||||
private static readonly ALLOWED = [
|
||||
'id',
|
||||
'note',
|
||||
'createdAt',
|
||||
'branchId',
|
||||
'branch.name',
|
||||
'allocatorAdminUser.fullName',
|
||||
// legacy filter keys
|
||||
'name',
|
||||
'caj',
|
||||
];
|
||||
|
||||
private static readonly FIELD_ALIASES: Record<string, string> = {
|
||||
name: 'branch.name',
|
||||
caj: 'createdAt',
|
||||
'allocator_user.name': 'allocatorAdminUser.fullName',
|
||||
};
|
||||
|
||||
/** Legacy `POST branch/product/table`. */
|
||||
async list(query: TableQueryDto) {
|
||||
this.table.assertNotExport(query);
|
||||
|
||||
const mapField = (f?: string) => (f ? (AdminInvoicesService.FIELD_ALIASES[f] ?? f) : f);
|
||||
const aliased: TableQueryDto = {
|
||||
...query,
|
||||
filters: (query.filters ?? []).map((f) => ({ ...f, field: mapField(f.field) })),
|
||||
order: query.order
|
||||
? `${query.order.startsWith('-') ? '-' : '+'}${mapField(query.order.replace(/^[+-]/, ''))}`
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const { where, orderBy, skip, take, page, limit } = this.table.build(aliased, {
|
||||
searchable: ['note', 'branch.name'],
|
||||
allowed: AdminInvoicesService.ALLOWED,
|
||||
defaultOrderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.branchAllocation.findMany({
|
||||
where: where as Prisma.BranchAllocationWhereInput,
|
||||
orderBy: orderBy as Prisma.BranchAllocationOrderByWithRelationInput,
|
||||
skip,
|
||||
take,
|
||||
include: {
|
||||
branch: true,
|
||||
allocatorAdminUser: true,
|
||||
items: { include: { product: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.branchAllocation.count({ where: where as Prisma.BranchAllocationWhereInput }),
|
||||
]);
|
||||
|
||||
return this.table.respond(rows.map((r) => this.toRow(r)), total, query, page, limit);
|
||||
}
|
||||
|
||||
private toRow(
|
||||
a: Prisma.BranchAllocationGetPayload<{
|
||||
include: {
|
||||
branch: true;
|
||||
allocatorAdminUser: true;
|
||||
items: { include: { product: true } };
|
||||
};
|
||||
}>,
|
||||
) {
|
||||
const sumWeight = a.items.reduce(
|
||||
(sum, it) => sum.add(it.weightGrams ?? new Prisma.Decimal(0)),
|
||||
new Prisma.Decimal(0),
|
||||
);
|
||||
|
||||
return {
|
||||
id: a.id,
|
||||
// legacy column key is `name` — the receiving branch
|
||||
name: a.branch.name,
|
||||
branch_id: a.branchId,
|
||||
branch: { id: a.branch.id, name: a.branch.name },
|
||||
sum_weight: sumWeight,
|
||||
count_item: a.items.length,
|
||||
note: a.note,
|
||||
allocator_user: a.allocatorAdminUser
|
||||
? { id: a.allocatorAdminUser.id, name: a.allocatorAdminUser.fullName }
|
||||
: null,
|
||||
caj: a.createdAt,
|
||||
products: a.items.map((it) => ({
|
||||
id: it.productId,
|
||||
pid: it.productId,
|
||||
title: it.product.title,
|
||||
product_code: it.product.productCode,
|
||||
grams: it.weightGrams,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/** Legacy `GET branch/product/{id}`. */
|
||||
async findOne(id: string) {
|
||||
const a = await this.prisma.branchAllocation.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
branch: true,
|
||||
allocatorAdminUser: true,
|
||||
items: { include: { product: true } },
|
||||
},
|
||||
});
|
||||
if (!a) throw new NotFoundException('فاکتور یافت نشد');
|
||||
return this.toRow(a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy `POST branch/product` with `{ branch_id, products: [id...] }`.
|
||||
*
|
||||
* Each product's current lightest variant weight is snapshotted, so a later
|
||||
* catalog edit does not silently rewrite what this document says was moved.
|
||||
*/
|
||||
async create(data: { branch_id?: string; products?: string[]; note?: string }, actorId: string) {
|
||||
const branchId = String(data.branch_id ?? '');
|
||||
if (!branchId) throw new BadRequestException('شعبه انتخاب نشده است');
|
||||
|
||||
const branch = await this.prisma.branch.findUnique({ where: { id: branchId } });
|
||||
if (!branch) throw new NotFoundException('شعبه یافت نشد');
|
||||
|
||||
const productIds = [...new Set((data.products ?? []).map((p) => String(p)).filter(Boolean))];
|
||||
if (productIds.length === 0) throw new BadRequestException('هیچ محصولی انتخاب نشده است');
|
||||
|
||||
const products = await this.prisma.product.findMany({
|
||||
where: { id: { in: productIds } },
|
||||
include: { variants: { where: { isActive: true } } },
|
||||
});
|
||||
if (products.length !== productIds.length) {
|
||||
throw new BadRequestException('برخی از محصولات انتخابشده یافت نشدند');
|
||||
}
|
||||
|
||||
const allocation = await this.prisma.branchAllocation.create({
|
||||
data: {
|
||||
branchId,
|
||||
allocatorAdminUserId: actorId,
|
||||
note: data.note ?? null,
|
||||
items: {
|
||||
create: products.map((p) => ({
|
||||
productId: p.id,
|
||||
weightGrams: p.variants.length
|
||||
? p.variants.reduce(
|
||||
(min, v) => (v.rashWeightGrams.lt(min) ? v.rashWeightGrams : min),
|
||||
p.variants[0].rashWeightGrams,
|
||||
)
|
||||
: null,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return this.findOne(allocation.id);
|
||||
}
|
||||
|
||||
/** Legacy `PUT branch/product/{id}` — the edit screen replaces the product set. */
|
||||
async update(id: string, data: { branch_id?: string; products?: string[]; note?: string }) {
|
||||
const existing = await this.prisma.branchAllocation.findUnique({ where: { id } });
|
||||
if (!existing) throw new NotFoundException('فاکتور یافت نشد');
|
||||
|
||||
if (data.branch_id) {
|
||||
const branch = await this.prisma.branch.findUnique({
|
||||
where: { id: String(data.branch_id) },
|
||||
});
|
||||
if (!branch) throw new NotFoundException('شعبه یافت نشد');
|
||||
}
|
||||
|
||||
if (Array.isArray(data.products)) {
|
||||
const productIds = [...new Set(data.products.map((p) => String(p)).filter(Boolean))];
|
||||
if (productIds.length === 0) {
|
||||
throw new BadRequestException('هیچ محصولی انتخاب نشده است');
|
||||
}
|
||||
const products = await this.prisma.product.findMany({
|
||||
where: { id: { in: productIds } },
|
||||
include: { variants: { where: { isActive: true } } },
|
||||
});
|
||||
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.branchAllocationItem.deleteMany({ where: { allocationId: id } }),
|
||||
this.prisma.branchAllocationItem.createMany({
|
||||
data: products.map((p) => ({
|
||||
allocationId: id,
|
||||
productId: p.id,
|
||||
weightGrams: p.variants.length
|
||||
? p.variants.reduce(
|
||||
(min, v) => (v.rashWeightGrams.lt(min) ? v.rashWeightGrams : min),
|
||||
p.variants[0].rashWeightGrams,
|
||||
)
|
||||
: null,
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
await this.prisma.branchAllocation.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(data.branch_id ? { branchId: String(data.branch_id) } : {}),
|
||||
...(data.note !== undefined ? { note: data.note } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
/** Legacy `DELETE branch/product/{id}`. Items cascade. */
|
||||
async remove(id: string) {
|
||||
const a = await this.prisma.branchAllocation.findUnique({ where: { id } });
|
||||
if (!a) throw new NotFoundException('فاکتور یافت نشد');
|
||||
await this.prisma.branchAllocation.delete({ where: { id } });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/** Legacy `GET branch/list` — the branch picker for the invoice form. */
|
||||
async branchList() {
|
||||
const rows = await this.prisma.branch.findMany({
|
||||
where: { isActive: true },
|
||||
orderBy: { name: 'asc' },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user