feat: complete admin panel and backend admin api migration
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, Post } from '@nestjs/common';
|
||||
import { AdminCommentsService } from './admin-comments.service';
|
||||
import { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
import { RequirePermission } from '../auth/guards';
|
||||
|
||||
/**
|
||||
* Legacy endpoints (CommentController, routes/admin.php:149-152):
|
||||
* POST comment/table -> the moderation grid
|
||||
* POST comment/sts -> approve / reject
|
||||
* POST comment -> staff reply
|
||||
* DELETE comment/{id}
|
||||
*
|
||||
* All gated on the legacy rid `comment`.
|
||||
*/
|
||||
@Controller('admin/comments')
|
||||
export class AdminCommentsController {
|
||||
constructor(private readonly comments: AdminCommentsService) {}
|
||||
|
||||
@Post('list')
|
||||
@HttpCode(200)
|
||||
@RequirePermission('comment')
|
||||
list(@Body() query: TableQueryDto) {
|
||||
return this.comments.list(query);
|
||||
}
|
||||
|
||||
@Get('status-options')
|
||||
@RequirePermission('comment')
|
||||
statusOptions() {
|
||||
return this.comments.statusOptions();
|
||||
}
|
||||
|
||||
@Post('sts')
|
||||
@HttpCode(200)
|
||||
@RequirePermission('comment')
|
||||
changeStatus(@Body() body: { id: string; sts: number }) {
|
||||
return this.comments.changeStatus(body.id, body.sts);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HttpCode(200)
|
||||
@RequirePermission('comment')
|
||||
create(
|
||||
@Body() body: { product_id: string; customer_id: string; comment: string; reply_to?: string },
|
||||
) {
|
||||
return this.comments.create(body);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermission('comment')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.comments.findOne(id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission('comment')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.comments.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from '../../prisma/prisma.module';
|
||||
import { AdminCommentsController } from './admin-comments.controller';
|
||||
import { AdminCommentsService } from './admin-comments.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [AdminCommentsController],
|
||||
providers: [AdminCommentsService],
|
||||
})
|
||||
export class AdminCommentsModule {}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CommentStatus, 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';
|
||||
|
||||
/**
|
||||
* Product comment moderation — legacy views/customer/Comments/Comments.vue,
|
||||
* served by CommentController (`POST comment/table`, `POST comment/sts`,
|
||||
* `POST comment`, `DELETE comment/{id}`).
|
||||
*
|
||||
* Legacy Comment::$STS = [1 => 'در انتظار تایید', 2 => 'تایید شده', 3 => 'رد شده'].
|
||||
* The numeric codes are preserved on the wire (`sts`) so the ported view's
|
||||
* status dropdown posts unchanged, while the DB stores an enum.
|
||||
*/
|
||||
const STATUS_BY_CODE: Record<number, CommentStatus> = {
|
||||
1: CommentStatus.PENDING,
|
||||
2: CommentStatus.APPROVED,
|
||||
3: CommentStatus.REJECTED,
|
||||
};
|
||||
|
||||
const CODE_BY_STATUS: Record<CommentStatus, number> = {
|
||||
PENDING: 1,
|
||||
APPROVED: 2,
|
||||
REJECTED: 3,
|
||||
};
|
||||
|
||||
const STATUS_LABELS: Record<CommentStatus, string> = {
|
||||
PENDING: 'در انتظار تایید',
|
||||
APPROVED: 'تایید شده',
|
||||
REJECTED: 'رد شده',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AdminCommentsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly table: TableService,
|
||||
) {}
|
||||
|
||||
private static readonly ALLOWED = [
|
||||
'id',
|
||||
'status',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'body',
|
||||
'customer.fullName',
|
||||
'customer.mobile',
|
||||
'product.title',
|
||||
// legacy filter keys
|
||||
'sts',
|
||||
'comment',
|
||||
'caj',
|
||||
];
|
||||
|
||||
private static readonly FIELD_ALIASES: Record<string, string> = {
|
||||
sts: 'status',
|
||||
comment: 'body',
|
||||
caj: 'createdAt',
|
||||
uaj: 'updatedAt',
|
||||
'customer.name': 'customer.fullName',
|
||||
'product.name': 'product.title',
|
||||
};
|
||||
|
||||
/** Legacy `POST comment/table`. */
|
||||
async list(query: TableQueryDto) {
|
||||
this.table.assertNotExport(query);
|
||||
|
||||
const mapField = (f?: string) =>
|
||||
f ? (AdminCommentsService.FIELD_ALIASES[f] ?? f) : f;
|
||||
|
||||
const aliased: TableQueryDto = {
|
||||
...query,
|
||||
filters: (query.filters ?? []).map((f) => {
|
||||
const mapped = { ...f, field: mapField(f.field) };
|
||||
// legacy sent the numeric status code
|
||||
if (mapped.field === 'status' && mapped.value !== undefined) {
|
||||
mapped.value = STATUS_BY_CODE[Number(mapped.value)] ?? mapped.value;
|
||||
}
|
||||
return mapped;
|
||||
}),
|
||||
order: query.order
|
||||
? `${query.order.startsWith('-') ? '-' : '+'}${mapField(query.order.replace(/^[+-]/, ''))}`
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const { where, orderBy, skip, take, page, limit } = this.table.build(aliased, {
|
||||
searchable: ['body', 'customer.fullName', 'product.title'],
|
||||
allowed: AdminCommentsService.ALLOWED,
|
||||
defaultOrderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.comment.findMany({
|
||||
where: where as Prisma.CommentWhereInput,
|
||||
orderBy: orderBy as Prisma.CommentOrderByWithRelationInput,
|
||||
skip,
|
||||
take,
|
||||
include: { customer: true, product: true, replyTo: true, replies: true },
|
||||
}),
|
||||
this.prisma.comment.count({ where: where as Prisma.CommentWhereInput }),
|
||||
]);
|
||||
|
||||
return this.table.respond(rows.map((r) => this.toRow(r)), total, query, page, limit);
|
||||
}
|
||||
|
||||
private toRow(
|
||||
c: Prisma.CommentGetPayload<{
|
||||
include: { customer: true; product: true; replyTo: true; replies?: true };
|
||||
}> & { replies?: { id: string; body: string }[] },
|
||||
) {
|
||||
return {
|
||||
id: c.id,
|
||||
comment: c.body,
|
||||
body: c.body,
|
||||
sts: CODE_BY_STATUS[c.status],
|
||||
sts_str: STATUS_LABELS[c.status],
|
||||
status: c.status,
|
||||
reply_to: c.replyToId,
|
||||
// legacy view reads: text.length > 0 && text[0].comment !== null
|
||||
reply: (c.replies ?? []).map((r) => ({ id: r.id, comment: r.body })),
|
||||
product_id: c.productId,
|
||||
customer_id: c.customerId,
|
||||
product: { id: c.product.id, title: c.product.title, name: c.product.title },
|
||||
customer: {
|
||||
id: c.customer.id,
|
||||
name: c.customer.fullName,
|
||||
fname: c.customer.fullName,
|
||||
lname: '',
|
||||
mobile: c.customer.mobile,
|
||||
},
|
||||
caj: c.createdAt,
|
||||
uaj: c.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const c = await this.prisma.comment.findUnique({
|
||||
where: { id },
|
||||
include: { customer: true, product: true, replyTo: true, replies: true },
|
||||
});
|
||||
if (!c) throw new NotFoundException('دیدگاه یافت نشد');
|
||||
return this.toRow(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy `POST comment/sts` — moderate a comment.
|
||||
* Payload is `{ id, sts }` where sts is 1/2/3.
|
||||
*/
|
||||
async changeStatus(id: string, sts: number) {
|
||||
const c = await this.prisma.comment.findUnique({ where: { id } });
|
||||
if (!c) throw new NotFoundException('دیدگاه یافت نشد');
|
||||
|
||||
const status = STATUS_BY_CODE[Number(sts)];
|
||||
if (!status) {
|
||||
throw new NotFoundException('وضعیت نامعتبر است');
|
||||
}
|
||||
return this.prisma.comment.update({ where: { id }, data: { status } });
|
||||
}
|
||||
|
||||
/** Legacy `POST comment` — post a staff reply to a comment. */
|
||||
async create(data: {
|
||||
product_id: string;
|
||||
customer_id: string;
|
||||
comment: string;
|
||||
reply_to?: string;
|
||||
}) {
|
||||
return this.prisma.comment.create({
|
||||
data: {
|
||||
productId: data.product_id,
|
||||
customerId: data.customer_id,
|
||||
body: data.comment,
|
||||
replyToId: data.reply_to ?? null,
|
||||
// A staff reply is visible immediately; legacy did the same.
|
||||
status: CommentStatus.APPROVED,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Legacy `DELETE comment/{id}`. */
|
||||
async remove(id: string) {
|
||||
const c = await this.prisma.comment.findUnique({
|
||||
where: { id },
|
||||
include: { replies: true },
|
||||
});
|
||||
if (!c) throw new NotFoundException('دیدگاه یافت نشد');
|
||||
|
||||
// Remove replies first so the self-relation does not block the delete.
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.comment.deleteMany({ where: { replyToId: id } }),
|
||||
this.prisma.comment.delete({ where: { id } }),
|
||||
]);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/** Farsi labels for the status dropdown, keyed by legacy's numeric codes. */
|
||||
statusOptions() {
|
||||
return Object.entries(STATUS_LABELS).map(([status, title]) => ({
|
||||
value: CODE_BY_STATUS[status as CommentStatus],
|
||||
title,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user