managarten/services/mana-core-auth/src/feedback/feedback.service.ts
Wuesteon 9c47119535 Fix wrong type
import, make auth and chat work
2025-12-04 23:25:25 +01:00

281 lines
7 KiB
TypeScript

import { Injectable, NotFoundException, ConflictException, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { eq, and, desc, sql, count } from 'drizzle-orm';
import { getDb } from '../db/connection';
import { userFeedback, feedbackVotes } from '../db/schema';
import { CreateFeedbackDto } from './dto';
import type { FeedbackQueryDto } from './dto';
import { AiService } from '../ai/ai.service';
@Injectable()
export class FeedbackService {
private readonly logger = new Logger(FeedbackService.name);
constructor(
private configService: ConfigService,
private aiService: AiService
) {}
private getDb() {
const databaseUrl = this.configService.get<string>('database.url');
return getDb(databaseUrl!);
}
async createFeedback(userId: string, appId: string, dto: CreateFeedbackDto) {
const db = this.getDb();
// Use AI to generate title and category if not provided
let title = dto.title;
let category = dto.category;
if (!title || !category) {
this.logger.debug('Analyzing feedback with AI...');
const analysis = await this.aiService.analyzeFeedback(dto.feedbackText);
if (!title) {
title = analysis.title;
}
if (!category) {
category = analysis.category;
}
this.logger.debug(`AI generated: title="${title}", category="${category}"`);
}
const [feedback] = await db
.insert(userFeedback)
.values({
userId,
appId,
title,
feedbackText: dto.feedbackText,
category: category || 'feature',
deviceInfo: dto.deviceInfo,
})
.returning();
return {
success: true,
feedback: this.mapFeedback(feedback, false),
};
}
async getPublicFeedback(userId: string | null, query: FeedbackQueryDto) {
const db = this.getDb();
const { appId, status, category, sort = 'votes', limit = 20, offset = 0 } = query;
// Build conditions
const conditions = [eq(userFeedback.isPublic, true)];
if (appId) {
conditions.push(eq(userFeedback.appId, appId));
}
if (status) {
conditions.push(eq(userFeedback.status, status as any));
}
if (category) {
conditions.push(eq(userFeedback.category, category as any));
}
// Get feedback items
const feedbackItems = await db
.select()
.from(userFeedback)
.where(and(...conditions))
.orderBy(sort === 'votes' ? desc(userFeedback.voteCount) : desc(userFeedback.createdAt))
.limit(limit)
.offset(offset);
// Get total count
const [{ total }] = await db
.select({ total: count() })
.from(userFeedback)
.where(and(...conditions));
// Get user's votes (only if authenticated)
let votedFeedbackIds = new Set<string>();
if (userId) {
const feedbackIds = feedbackItems.map((f) => f.id);
const userVotes =
feedbackIds.length > 0
? await db
.select({ feedbackId: feedbackVotes.feedbackId })
.from(feedbackVotes)
.where(
and(
eq(feedbackVotes.userId, userId),
sql`${feedbackVotes.feedbackId} = ANY(${feedbackIds})`
)
)
: [];
votedFeedbackIds = new Set(userVotes.map((v) => v.feedbackId));
}
return {
success: true,
items: feedbackItems.map((f) => this.mapFeedback(f, votedFeedbackIds.has(f.id))),
total,
};
}
async getMyFeedback(userId: string, appId?: string) {
const db = this.getDb();
const conditions = [eq(userFeedback.userId, userId)];
if (appId) {
conditions.push(eq(userFeedback.appId, appId));
}
const feedbackItems = await db
.select()
.from(userFeedback)
.where(and(...conditions))
.orderBy(desc(userFeedback.createdAt));
// Get user's votes on their own feedback (for consistency)
const feedbackIds = feedbackItems.map((f) => f.id);
const userVotes =
feedbackIds.length > 0
? await db
.select({ feedbackId: feedbackVotes.feedbackId })
.from(feedbackVotes)
.where(
and(
eq(feedbackVotes.userId, userId),
sql`${feedbackVotes.feedbackId} = ANY(${feedbackIds})`
)
)
: [];
const votedFeedbackIds = new Set(userVotes.map((v) => v.feedbackId));
return {
success: true,
items: feedbackItems.map((f) => this.mapFeedback(f, votedFeedbackIds.has(f.id))),
total: feedbackItems.length,
};
}
async vote(userId: string, feedbackId: string) {
const db = this.getDb();
// Check if feedback exists and is public
const [feedback] = await db
.select()
.from(userFeedback)
.where(eq(userFeedback.id, feedbackId))
.limit(1);
if (!feedback) {
throw new NotFoundException('Feedback not found');
}
if (!feedback.isPublic) {
throw new NotFoundException('Feedback not found or not public');
}
// Check if user already voted
const [existingVote] = await db
.select()
.from(feedbackVotes)
.where(and(eq(feedbackVotes.feedbackId, feedbackId), eq(feedbackVotes.userId, userId)))
.limit(1);
if (existingVote) {
throw new ConflictException('Already voted');
}
// Add vote
await db.insert(feedbackVotes).values({
feedbackId,
userId,
});
// Increment vote count
const [updated] = await db
.update(userFeedback)
.set({
voteCount: sql`${userFeedback.voteCount} + 1`,
updatedAt: new Date(),
})
.where(eq(userFeedback.id, feedbackId))
.returning();
return {
success: true,
newVoteCount: updated.voteCount,
userHasVoted: true,
};
}
async unvote(userId: string, feedbackId: string) {
const db = this.getDb();
// Check if feedback exists
const [feedback] = await db
.select()
.from(userFeedback)
.where(eq(userFeedback.id, feedbackId))
.limit(1);
if (!feedback) {
throw new NotFoundException('Feedback not found');
}
// Check if user has voted
const [existingVote] = await db
.select()
.from(feedbackVotes)
.where(and(eq(feedbackVotes.feedbackId, feedbackId), eq(feedbackVotes.userId, userId)))
.limit(1);
if (!existingVote) {
throw new NotFoundException('Vote not found');
}
// Remove vote
await db
.delete(feedbackVotes)
.where(and(eq(feedbackVotes.feedbackId, feedbackId), eq(feedbackVotes.userId, userId)));
// Decrement vote count
const [updated] = await db
.update(userFeedback)
.set({
voteCount: sql`GREATEST(${userFeedback.voteCount} - 1, 0)`,
updatedAt: new Date(),
})
.where(eq(userFeedback.id, feedbackId))
.returning();
return {
success: true,
newVoteCount: updated.voteCount,
userHasVoted: false,
};
}
private mapFeedback(
feedback: typeof userFeedback.$inferSelect,
userHasVoted: boolean
): Record<string, unknown> {
return {
id: feedback.id,
userId: feedback.userId,
appId: feedback.appId,
title: feedback.title,
feedbackText: feedback.feedbackText,
category: feedback.category,
status: feedback.status,
isPublic: feedback.isPublic,
adminResponse: feedback.adminResponse,
voteCount: feedback.voteCount,
userHasVoted,
deviceInfo: feedback.deviceInfo,
createdAt: feedback.createdAt.toISOString(),
updatedAt: feedback.updatedAt.toISOString(),
publishedAt: feedback.publishedAt?.toISOString(),
completedAt: feedback.completedAt?.toISOString(),
};
}
}