feat: Add functionality to move inventory items to pantry and enhance pantry management
Test Suite / test (24.15.0) (push) Has been cancelled

- Implemented moveInventoryItemToPantry method in InventoryRepository to facilitate moving items from inventory to pantry.
- Enhanced InventoryScreen with a new header section providing context about the inventory.
- Added a button in SwipeableInventoryTile to move items to pantry with appropriate error handling.
- Introduced movePantryItemToInventory method in PantryRepository to support moving items back to inventory.
- Refactored PantryScreen to rename _addToInventory to _moveToInventory for clarity and updated UI to reflect changes.
- Added AdminPantryItem model to represent pantry items in the admin panel.
- Created AdminPantryPanel for managing pantry items, including moving items to inventory and listing users.
- Developed AdminPrivateProductsPanel for managing private products, allowing promotion to global products.
This commit is contained in:
Nils-Johan Gynther
2026-05-11 09:06:30 +02:00
parent edf9c74e75
commit 84ccabe2fe
27 changed files with 1851 additions and 376 deletions
@@ -79,6 +79,12 @@ export class ProductsController {
return this.productsService.findPending();
}
@Roles('admin')
@Get('private')
findPrivate() {
return this.productsService.findPrivate();
}
@Roles('admin')
@Post('ai-categorize-bulk')
@Throttle({ default: { ttl: 60_000, limit: 5 } })
@@ -202,6 +208,15 @@ export class ProductsController {
return this.productsService.update(id, body);
}
@Roles('admin')
@Post('private/:id/promote')
promotePrivateToGlobal(
@Param('id', ParseIntPipe) id: number,
@Request() req: { user: { id: number } },
) {
return this.productsService.promotePrivateToGlobal(id, req.user.id);
}
@Roles('admin')
@Delete(':id/permanent')
permanentDelete(@Param('id', ParseIntPipe) id: number) {
+72
View File
@@ -435,6 +435,17 @@ export class ProductsService {
});
}
async findPrivate() {
return this.prisma.product.findMany({
where: { isPrivate: true, isActive: true },
include: {
categoryRef: { include: { parent: true } },
owner: { select: { id: true, username: true } },
},
orderBy: { createdAt: 'desc' },
});
}
async createPending(data: CreateProductDto, userId: number) {
const name = data.name.trim();
const normalizedName = normalizeName(name);
@@ -470,6 +481,67 @@ export class ProductsService {
return this.prisma.product.update({ where: { id }, data: { status } });
}
async promotePrivateToGlobal(productId: number, adminUserId: number) {
const source = await this.prisma.product.findUnique({
where: { id: productId },
select: {
id: true,
name: true,
canonicalName: true,
normalizedName: true,
categoryId: true,
isPrivate: true,
isActive: true,
ownerId: true,
},
});
if (!source) {
throw new NotFoundException(`Product with id ${productId} not found`);
}
if (!source.isPrivate) {
throw new ForbiddenException('Endast privata produkter kan promoveras till global produkt');
}
const name = (source.canonicalName ?? source.name).trim();
const normalizedName = normalizeName(name);
const existingGlobal = await this.prisma.product.findUnique({
where: { normalizedName },
});
if (existingGlobal && existingGlobal.id !== source.id) {
if (!existingGlobal.isActive) {
return this.prisma.product.update({
where: { id: existingGlobal.id },
data: {
isActive: true,
deletedAt: null,
name,
canonicalName: name,
categoryId: source.categoryId ?? existingGlobal.categoryId,
},
});
}
return existingGlobal;
}
return this.prisma.product.create({
data: {
name,
normalizedName,
canonicalName: name,
isActive: true,
isPrivate: false,
ownerId: adminUserId,
deletedAt: null,
...(source.categoryId != null ? { categoryId: source.categoryId } : {}),
},
});
}
// ── Privata produkter (användare kan hantera sina egna) ──────────────────────
// Hjälpfunktioner för att undvika kodduplicering mellan admin och user-scope