MAJOR UPPDATE: "First Ai"
feat: add AI categorization for products and enhance user management - Integrated AI service for category suggestions in receipt import and product management. - Added premium subscription feature for users with corresponding API endpoints. - Implemented admin interface for managing pending product suggestions. - Enhanced user management to include premium status and corresponding UI updates. - Updated database schema to support new fields for premium status and product status.
This commit is contained in:
@@ -2,6 +2,7 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { CreateProductDto } from './dto/create-product.dto';
|
||||
import { UpdateProductDto } from './dto/update-product.dto';
|
||||
@@ -20,10 +22,29 @@ import { SetTagsDto } from './dto/set-tags.dto';
|
||||
import { UpsertNutritionDto } from './dto/upsert-nutrition.dto';
|
||||
import { BulkUpdateProductsDto } from './dto/bulk-update-products.dto';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import { AiService } from '../ai/ai.service';
|
||||
import { CategoriesService } from '../categories/categories.service';
|
||||
import { IsArray, IsIn, IsInt, IsOptional } from 'class-validator';
|
||||
|
||||
class AiCategorizeBulkDto {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
productIds?: number[];
|
||||
}
|
||||
|
||||
class SetProductStatusDto {
|
||||
@IsIn(['active', 'rejected'])
|
||||
status: string;
|
||||
}
|
||||
|
||||
@Controller('products')
|
||||
export class ProductsController {
|
||||
constructor(private readonly productsService: ProductsService) {}
|
||||
constructor(
|
||||
private readonly productsService: ProductsService,
|
||||
private readonly aiService: AiService,
|
||||
private readonly categoriesService: CategoriesService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
findAll(
|
||||
@@ -57,11 +78,50 @@ export class ProductsController {
|
||||
return this.productsService.backfillCanonicalNames();
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Get('pending')
|
||||
findPending() {
|
||||
return this.productsService.findPending();
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Post('ai-categorize-bulk')
|
||||
@HttpCode(200)
|
||||
async aiCategorizeBulk(@Body() body: AiCategorizeBulkDto) {
|
||||
const categories = await this.categoriesService.findFlattened();
|
||||
let products: { id: number; name: string }[];
|
||||
if (body.productIds && body.productIds.length > 0) {
|
||||
const found = await Promise.all(body.productIds.map((id) => this.productsService.findOne(id)));
|
||||
products = found.map((p) => ({ id: p.id, name: p.canonicalName ?? p.name }));
|
||||
} else {
|
||||
products = await this.productsService.findUncategorized();
|
||||
}
|
||||
const results: { productId: number; productName: string; suggestion: object }[] = [];
|
||||
for (const product of products) {
|
||||
const suggestion = await this.aiService.suggestCategory(product.name, categories);
|
||||
results.push({ productId: product.id, productName: product.name, suggestion });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.productsService.findOne(id);
|
||||
}
|
||||
|
||||
@Get(':id/suggest-category')
|
||||
async suggestCategory(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Request() req: { user: { role: string; isPremium: boolean } },
|
||||
) {
|
||||
if (req.user.role !== 'admin' && !req.user.isPremium) {
|
||||
throw new ForbiddenException('Denna funktion kräver premiumkonto');
|
||||
}
|
||||
const product = await this.productsService.findOne(id);
|
||||
const categories = await this.categoriesService.findFlattened();
|
||||
return this.aiService.suggestCategory(product.canonicalName ?? product.name, categories);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() body: CreateProductDto) {
|
||||
return this.productsService.create(body);
|
||||
@@ -111,6 +171,15 @@ export class ProductsController {
|
||||
return this.productsService.remove(id);
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Patch(':id/status')
|
||||
setStatus(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() body: SetProductStatusDto,
|
||||
) {
|
||||
return this.productsService.setStatus(id, body.status);
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Post(':id/restore')
|
||||
restore(@Param('id', ParseIntPipe) id: number) {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ProductsController } from './products.controller';
|
||||
import { ProductsService } from './products.service';
|
||||
import { AiModule } from '../ai/ai.module';
|
||||
import { CategoriesModule } from '../categories/categories.module';
|
||||
|
||||
@Module({
|
||||
imports: [AiModule, CategoriesModule],
|
||||
controllers: [ProductsController],
|
||||
providers: [ProductsService],
|
||||
})
|
||||
|
||||
@@ -407,4 +407,27 @@ export class ProductsService {
|
||||
await this.prisma.product.updateMany({ where: { id: { in: ids } }, data: updateData });
|
||||
return { updated: ids.length };
|
||||
}
|
||||
|
||||
async findUncategorized(): Promise<{ id: number; name: string; canonicalName: string | null }[]> {
|
||||
return this.prisma.product.findMany({
|
||||
where: { isActive: true, categoryId: null, status: 'active' },
|
||||
select: { id: true, name: true, canonicalName: true },
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findPending() {
|
||||
return this.prisma.product.findMany({
|
||||
where: { status: 'pending' },
|
||||
include: {
|
||||
categoryRef: { include: { parent: true } },
|
||||
owner: { select: { id: true, username: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
setStatus(id: number, status: string) {
|
||||
return this.prisma.product.update({ where: { id }, data: { status } });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user