5801cbf85b
- KnowledgeBoard + KnowledgeItem models + migration (DRAFT/PUBLISHED/ARCHIVED status, slug per tenant) - KnowledgeModule: admin CRUD for boards + items (auto-slug, status workflow) - Member endpoint GET /my/knowledge?q= — published items grouped by board, optional text search over question/answer - /my/knowledge page: search box + collapsible boards with accordion FAQ items - KnowledgeItem accordion + KnowledgeSearch (debounced URL-driven) client components - Knowledge nav item; register KnowledgeModule Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
155 lines
5.6 KiB
TypeScript
155 lines
5.6 KiB
TypeScript
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
|
import { PrismaService } from '../../prisma/prisma.service';
|
|
|
|
function slugify(name: string): string {
|
|
return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'board';
|
|
}
|
|
|
|
@Injectable()
|
|
export class KnowledgeService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
// ── Admin: boards ──────────────────────────────────────────────────────────
|
|
|
|
async listBoardsAdmin(tenantId: string) {
|
|
return this.prisma.knowledgeBoard.findMany({
|
|
where: { tenantId },
|
|
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }],
|
|
include: { _count: { select: { items: true } } },
|
|
});
|
|
}
|
|
|
|
async createBoard(tenantId: string, dto: { name: string; description?: string; sortOrder?: number }) {
|
|
let slug = slugify(dto.name);
|
|
const existing = await this.prisma.knowledgeBoard.findFirst({ where: { tenantId, slug } });
|
|
if (existing) slug = `${slug}-${Date.now().toString(36)}`;
|
|
|
|
return this.prisma.knowledgeBoard.create({
|
|
data: {
|
|
tenantId,
|
|
name: dto.name,
|
|
description: dto.description ?? null,
|
|
slug,
|
|
sortOrder: dto.sortOrder ?? 0,
|
|
},
|
|
});
|
|
}
|
|
|
|
async updateBoard(tenantId: string, id: string, dto: { name?: string; description?: string; sortOrder?: number }) {
|
|
const board = await this.prisma.knowledgeBoard.findFirst({ where: { id, tenantId } });
|
|
if (!board) throw new NotFoundException('Board not found');
|
|
return this.prisma.knowledgeBoard.update({
|
|
where: { id },
|
|
data: {
|
|
...(dto.name !== undefined && { name: dto.name }),
|
|
...(dto.description !== undefined && { description: dto.description }),
|
|
...(dto.sortOrder !== undefined && { sortOrder: dto.sortOrder }),
|
|
},
|
|
});
|
|
}
|
|
|
|
async removeBoard(tenantId: string, id: string) {
|
|
const board = await this.prisma.knowledgeBoard.findFirst({ where: { id, tenantId } });
|
|
if (!board) throw new NotFoundException('Board not found');
|
|
await this.prisma.knowledgeBoard.delete({ where: { id } });
|
|
return { ok: true };
|
|
}
|
|
|
|
// ── Admin: items ─────────────────────────────────────────────────────────────
|
|
|
|
async listItemsAdmin(tenantId: string, boardId: string) {
|
|
const board = await this.prisma.knowledgeBoard.findFirst({ where: { id: boardId, tenantId } });
|
|
if (!board) throw new NotFoundException('Board not found');
|
|
return this.prisma.knowledgeItem.findMany({
|
|
where: { boardId },
|
|
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }],
|
|
});
|
|
}
|
|
|
|
async createItem(tenantId: string, adminId: string, boardId: string, dto: {
|
|
question: string;
|
|
answer: string;
|
|
tags?: string[];
|
|
status?: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
|
|
sortOrder?: number;
|
|
}) {
|
|
const board = await this.prisma.knowledgeBoard.findFirst({ where: { id: boardId, tenantId } });
|
|
if (!board) throw new NotFoundException('Board not found');
|
|
return this.prisma.knowledgeItem.create({
|
|
data: {
|
|
boardId,
|
|
tenantId,
|
|
question: dto.question,
|
|
answer: dto.answer,
|
|
tags: dto.tags ?? [],
|
|
status: dto.status ?? 'DRAFT',
|
|
sortOrder: dto.sortOrder ?? 0,
|
|
createdBy: adminId,
|
|
},
|
|
});
|
|
}
|
|
|
|
async updateItem(tenantId: string, itemId: string, dto: {
|
|
question?: string;
|
|
answer?: string;
|
|
tags?: string[];
|
|
status?: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
|
|
sortOrder?: number;
|
|
}) {
|
|
const item = await this.prisma.knowledgeItem.findFirst({ where: { id: itemId, tenantId } });
|
|
if (!item) throw new NotFoundException('Item not found');
|
|
return this.prisma.knowledgeItem.update({
|
|
where: { id: itemId },
|
|
data: {
|
|
...(dto.question !== undefined && { question: dto.question }),
|
|
...(dto.answer !== undefined && { answer: dto.answer }),
|
|
...(dto.tags !== undefined && { tags: dto.tags }),
|
|
...(dto.status !== undefined && { status: dto.status }),
|
|
...(dto.sortOrder !== undefined && { sortOrder: dto.sortOrder }),
|
|
},
|
|
});
|
|
}
|
|
|
|
async removeItem(tenantId: string, itemId: string) {
|
|
const item = await this.prisma.knowledgeItem.findFirst({ where: { id: itemId, tenantId } });
|
|
if (!item) throw new NotFoundException('Item not found');
|
|
await this.prisma.knowledgeItem.delete({ where: { id: itemId } });
|
|
return { ok: true };
|
|
}
|
|
|
|
// ── Member: read published knowledge grouped by board ────────────────────────
|
|
|
|
async listForMember(tenantId: string, q?: string) {
|
|
const boards = await this.prisma.knowledgeBoard.findMany({
|
|
where: { tenantId },
|
|
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }],
|
|
include: {
|
|
items: {
|
|
where: {
|
|
status: 'PUBLISHED',
|
|
...(q && q.trim()
|
|
? {
|
|
OR: [
|
|
{ question: { contains: q.trim(), mode: 'insensitive' } },
|
|
{ answer: { contains: q.trim(), mode: 'insensitive' } },
|
|
],
|
|
}
|
|
: {}),
|
|
},
|
|
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }],
|
|
select: { id: true, question: true, answer: true, tags: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
return boards
|
|
.filter((b) => b.items.length > 0)
|
|
.map((b) => ({
|
|
id: b.id,
|
|
name: b.name,
|
|
description: b.description,
|
|
items: b.items,
|
|
}));
|
|
}
|
|
}
|