Files
hMarket/backend/src/routes/items.js
2026-02-03 01:22:08 +03:00

653 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const { PrismaClient } = require('@prisma/client');
const { authenticateToken, checkListMembership, requireListEditPermission } = require('../middleware/auth');
const { asyncHandler } = require('../middleware/errorHandler');
const {
validateListItemCreation,
validateListItemUpdate,
validateUUIDParam,
validateCuidParam,
validateCuid,
validatePagination
} = require('../utils/validators');
const { validationResult, param } = require('express-validator');
const { successResponse, errorResponse, calculatePagination, createPaginationMeta } = require('../utils/helpers');
const notificationService = require('../services/notificationService');
const router = express.Router();
const prisma = new PrismaClient();
/**
* Liste öğelerini getir
* GET /api/items/:listId
*/
router.get('/:listId',
authenticateToken,
param('listId').custom(validateCuid).withMessage('Geçerli bir liste ID\'si girin'),
validatePagination,
checkListMembership,
asyncHandler(async (req, res) => {
console.log('🔍 Items API called with params:', req.params);
console.log('🔍 Items API called with query:', req.query);
const errors = validationResult(req);
if (!errors.isEmpty()) {
console.log('❌ Validation errors:', errors.array());
return res.status(400).json(errorResponse('Doğrulama hatası', errors.array()));
}
const { listId } = req.params;
const { page = 1, limit = 50, category, purchased, search } = req.query;
const { skip, take } = calculatePagination(page, limit);
// Filtreleme koşulları
const where = {
listId
};
if (category) {
where.product = {
categoryId: category
};
}
if (purchased !== undefined) {
where.isPurchased = purchased === 'true';
}
if (search) {
where.OR = [
{ name: { contains: search, mode: 'insensitive' } },
{ notes: { contains: search, mode: 'insensitive' } },
{ product: { name: { contains: search, mode: 'insensitive' } } }
];
}
console.log('🔍 Database where conditions:', JSON.stringify(where, null, 2));
console.log('🔍 Pagination - skip:', skip, 'take:', take);
// Toplam sayı ve öğeleri getir
const [total, items] = await Promise.all([
prisma.listItem.count({ where }),
prisma.listItem.findMany({
where,
skip,
take,
include: {
product: {
include: {
category: true
}
}
},
orderBy: [
{ isPurchased: 'asc' },
{ createdAt: 'desc' }
]
})
]);
const meta = createPaginationMeta(total, parseInt(page), parseInt(limit));
// Priority değerlerini string'e çevir
const itemsWithStringPriority = items.map(item => ({
...item,
priority: item.priority === 0 ? 'LOW' : item.priority === 1 ? 'MEDIUM' : 'HIGH'
}));
res.json(successResponse('Liste öğeleri başarıyla getirildi', itemsWithStringPriority, meta));
})
);
/**
* Liste öğesi detayını getir
* GET /api/items/:listId/:itemId
*/
router.get('/:listId/:itemId',
authenticateToken,
param('listId').custom(validateCuid).withMessage('Geçerli bir liste ID\'si girin'),
param('itemId').custom(validateCuid).withMessage('Geçerli bir öğe ID\'si girin'),
checkListMembership,
asyncHandler(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json(errorResponse('Doğrulama hatası', errors.array()));
}
const { listId, itemId } = req.params;
const item = await prisma.listItem.findFirst({
where: {
id: itemId,
listId
},
include: {
product: {
include: {
category: true,
priceHistory: {
orderBy: { createdAt: 'desc' },
take: 10
}
}
},
addedBy: {
select: {
id: true,
username: true,
firstName: true,
lastName: true
}
}
}
});
if (!item) {
return res.status(404).json(errorResponse('Liste öğesi bulunamadı'));
}
// Priority değerini string'e çevir
const itemWithStringPriority = {
...item,
priority: item.priority === 0 ? 'LOW' : item.priority === 1 ? 'MEDIUM' : 'HIGH'
};
res.json(successResponse('Liste öğesi detayı başarıyla getirildi', itemWithStringPriority));
})
);
/**
* Listeye öğe ekle
* POST /api/items/:listId
*/
router.post('/:listId',
authenticateToken,
param('listId').custom(validateCuid).withMessage('Geçerli bir liste ID\'si girin'),
validateListItemCreation,
checkListMembership,
requireListEditPermission,
asyncHandler(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json(errorResponse('Doğrulama hatası', errors.array()));
}
const { listId } = req.params;
const { name, quantity = 1, unit, notes, productId, estimatedPrice } = req.body;
const userId = req.user.id;
// Eğer productId verilmişse, ürünün var olduğunu kontrol et
let product = null;
if (productId) {
product = await prisma.product.findUnique({
where: { id: productId }
});
if (!product) {
return res.status(404).json(errorResponse('Ürün bulunamadı'));
}
}
// Aynı öğenin listede zaten var olup olmadığını kontrol et
const existingItem = await prisma.listItem.findFirst({
where: {
listId,
OR: [
{ productId: productId || undefined },
{ customName: productId ? undefined : name }
]
}
});
if (existingItem) {
return res.status(409).json(errorResponse('Bu öğe zaten listede mevcut'));
}
// Yeni öğe oluştur
const newItem = await prisma.listItem.create({
data: {
customName: productId ? product.name : name,
quantity,
unit: unit || "adet",
note: notes,
price: estimatedPrice,
listId,
productId
},
include: {
product: {
include: {
category: true
}
}
}
});
// Ürün kullanım sayısını artır (Product modelinde usageCount alanı yok, bu özellik kaldırıldı)
// Liste güncelleme tarihini güncelle
await prisma.shoppingList.update({
where: { id: listId },
data: { updatedAt: new Date() }
});
// Aktivite kaydı oluştur
await prisma.activity.create({
data: {
action: 'item_added',
details: {
itemId: newItem.id,
itemName: newItem.customName || newItem.product?.name || 'Öğe',
userName: `${req.user.firstName} ${req.user.lastName}`
},
userId,
listId
}
});
// Liste üyelerine bildirim gönder (geçici olarak devre dışı - notifyListMembers fonksiyonu mevcut değil)
// await notificationService.notifyListMembers(
// listId,
// userId,
// 'ITEM_ADDED',
// `${req.user.firstName} ${req.user.lastName} listeye "${newItem.customName || newItem.product?.name || 'Öğe'}" öğesini ekledi`,
// { itemId: newItem.id, itemName: newItem.customName || newItem.product?.name }
// );
// Socket.IO ile gerçek zamanlı güncelleme
const io = req.app.get('io');
if (io) {
io.to(`list_${listId}`).emit('itemAdded', {
item: newItem,
addedBy: req.user
});
}
// Priority değerini string'e çevir
const newItemWithStringPriority = {
...newItem,
priority: newItem.priority === 0 ? 'LOW' : newItem.priority === 1 ? 'MEDIUM' : 'HIGH'
};
res.status(201).json(successResponse('Öğe başarıyla eklendi', newItemWithStringPriority));
})
);
/**
* Liste öğesini güncelle
* PUT /api/items/:listId/:itemId
*/
router.put('/:listId/:itemId',
authenticateToken,
param('listId').custom(validateCuid).withMessage('Geçerli bir liste ID\'si girin'),
param('itemId').custom(validateCuid).withMessage('Geçerli bir öğe ID\'si girin'),
validateListItemUpdate,
checkListMembership,
// Sadece isPurchased güncellemesi değilse edit yetkisi gerekli
(req, res, next) => {
const { name, quantity, unit, notes, price, priority } = req.body;
const isOnlyPurchaseUpdate = !name && !quantity && !unit && !notes && !price && !priority;
if (isOnlyPurchaseUpdate) {
// Sadece isPurchased güncellemesi - tüm üyeler yapabilir
return next();
} else {
// Diğer alanlar güncelleniyor - edit yetkisi gerekli
const allowedRoles = ['owner', 'admin'];
if (!allowedRoles.includes(req.userRole)) {
return res.status(403).json({
success: false,
message: 'Bu işlem için yeterli yetkiniz yok.'
});
}
return next();
}
},
asyncHandler(async (req, res) => {
console.log('🔍 PUT /api/items/:listId/:itemId başladı');
console.log('📝 Request params:', req.params);
console.log('📝 Request body:', req.body);
console.log('👤 User ID:', req.user.id);
const errors = validationResult(req);
if (!errors.isEmpty()) {
console.log('❌ Validation errors:', errors.array());
return res.status(400).json(errorResponse('Doğrulama hatası', errors.array()));
}
const { listId, itemId } = req.params;
const { name, quantity, unit, notes, isPurchased, price, priority } = req.body;
const userId = req.user.id;
// Öğenin var olduğunu kontrol et
console.log('🔍 Öğe aranıyor:', { itemId, listId });
const existingItem = await prisma.listItem.findFirst({
where: {
id: itemId,
listId
},
include: {
product: true
}
});
if (!existingItem) {
console.log('❌ Öğe bulunamadı');
return res.status(404).json(errorResponse('Liste öğesi bulunamadı'));
}
console.log('✅ Öğe bulundu:', existingItem.name);
// Güncelleme verilerini hazırla
const updateData = {};
if (name !== undefined) updateData.customName = name;
if (quantity !== undefined) updateData.quantity = quantity;
if (unit !== undefined) updateData.unit = unit;
if (notes !== undefined) updateData.note = notes;
if (price !== undefined) updateData.price = price;
if (priority !== undefined) {
// Priority string'i sayıya çevir
const priorityMap = { 'LOW': 0, 'MEDIUM': 1, 'HIGH': 2 };
updateData.priority = priorityMap[priority] !== undefined ? priorityMap[priority] : 1;
}
// Satın alma durumu değişikliği
if (isPurchased !== undefined && isPurchased !== existingItem.isPurchased) {
updateData.isPurchased = isPurchased;
if (isPurchased) {
updateData.purchasedAt = new Date();
updateData.purchasedBy = userId;
} else {
updateData.purchasedAt = null;
updateData.purchasedBy = null;
}
}
// Öğeyi güncelle
const updatedItem = await prisma.listItem.update({
where: { id: itemId },
data: updateData,
include: {
product: {
include: {
category: true
}
}
}
});
// Fiyat geçmişi ekle (eğer fiyat girilmişse ve ürün varsa)
if (price && existingItem.productId && isPurchased) {
await prisma.priceHistory.create({
data: {
price: price,
productId: existingItem.productId,
userId,
location: 'Market' // Varsayılan konum
}
});
}
// Liste güncelleme tarihini güncelle
await prisma.shoppingList.update({
where: { id: listId },
data: { updatedAt: new Date() }
});
// Aktivite kaydı oluştur
let activityDescription = `${req.user.firstName} ${req.user.lastName} "${updatedItem.name}" öğesini güncelledi`;
if (isPurchased !== undefined) {
activityDescription = isPurchased
? `${req.user.firstName} ${req.user.lastName} "${updatedItem.name}" öğesini satın aldı`
: `${req.user.firstName} ${req.user.lastName} "${updatedItem.name}" öğesinin satın alma durumunu iptal etti`;
}
await prisma.activity.create({
data: {
action: isPurchased !== undefined ? (isPurchased ? 'ITEM_PURCHASED' : 'ITEM_UNPURCHASED') : 'ITEM_UPDATED',
details: {
description: activityDescription,
itemId: updatedItem.id,
itemName: updatedItem.name
},
userId,
listId
}
});
// Liste üyelerine bildirim gönder (sadece satın alma durumu değişikliğinde)
if (isPurchased !== undefined) {
await notificationService.notifyListMembers(
listId,
userId,
{
type: isPurchased ? 'ITEM_PURCHASED' : 'ITEM_UNPURCHASED',
message: activityDescription,
data: { itemId: updatedItem.id, itemName: updatedItem.name }
}
);
}
// Socket.IO ile gerçek zamanlı güncelleme
const io = req.app.get('io');
if (io) {
io.to(`list_${listId}`).emit('itemUpdated', {
item: updatedItem,
updatedBy: req.user
});
}
// Priority değerini string'e çevir
const updatedItemWithStringPriority = {
...updatedItem,
priority: updatedItem.priority === 0 ? 'LOW' : updatedItem.priority === 1 ? 'MEDIUM' : 'HIGH'
};
res.json(successResponse('Öğe başarıyla güncellendi', updatedItemWithStringPriority));
})
);
/**
* Liste öğesini sil
* DELETE /api/items/:listId/:itemId
*/
router.delete('/:listId/:itemId',
authenticateToken,
param('listId').custom(validateCuid).withMessage('Geçerli bir liste ID\'si girin'),
param('itemId').custom(validateCuid).withMessage('Geçerli bir öğe ID\'si girin'),
checkListMembership,
requireListEditPermission,
asyncHandler(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json(errorResponse('Doğrulama hatası', errors.array()));
}
const { listId, itemId } = req.params;
const userId = req.user.id;
// Öğenin var olduğunu kontrol et
const existingItem = await prisma.listItem.findFirst({
where: {
id: itemId,
listId
}
});
if (!existingItem) {
return res.status(404).json(errorResponse('Liste öğesi bulunamadı'));
}
// Öğeyi sil
await prisma.listItem.delete({
where: { id: itemId }
});
// Liste güncelleme tarihini güncelle
await prisma.shoppingList.update({
where: { id: listId },
data: { updatedAt: new Date() }
});
// Aktivite kaydı oluştur (Prisma şemasına uygun)
const itemName = existingItem.customName || existingItem.product?.name || 'Öğe';
await prisma.activity.create({
data: {
action: 'ITEM_REMOVED',
details: {
description: `${req.user.firstName} ${req.user.lastName} "${itemName}" öğesini listeden kaldırdı`,
itemId: existingItem.id,
itemName: itemName
},
userId,
listId
}
});
// Liste üyelerine bildirim gönder
await notificationService.notifyListMembers(
listId,
userId,
{
type: 'ITEM_REMOVED',
message: `${req.user.firstName} ${req.user.lastName} "${itemName}" öğesini listeden kaldırdı`,
data: { itemId: existingItem.id, itemName: itemName }
}
);
// Socket.IO ile gerçek zamanlı güncelleme
const io = req.app.get('io');
if (io) {
io.to(`list_${listId}`).emit('itemRemoved', {
itemId: existingItem.id,
itemName: existingItem.name,
removedBy: req.user
});
}
res.json(successResponse('Öğe başarıyla silindi'));
})
);
/**
* Birden fazla öğeyi toplu güncelle
* PATCH /api/items/:listId/bulk
*/
router.patch('/:listId/bulk',
authenticateToken,
validateCuidParam('listId'),
requireListEditPermission,
asyncHandler(async (req, res) => {
const { listId } = req.params;
const { items, action } = req.body; // items: [itemId1, itemId2], action: 'purchase' | 'unpurchase' | 'delete'
const userId = req.user.id;
if (!items || !Array.isArray(items) || items.length === 0) {
return res.status(400).json(errorResponse('Geçerli öğe listesi gerekli'));
}
if (!['purchase', 'unpurchase', 'delete'].includes(action)) {
return res.status(400).json(errorResponse('Geçerli bir işlem seçin'));
}
// Öğelerin var olduğunu kontrol et
const existingItems = await prisma.listItem.findMany({
where: {
id: { in: items },
listId
}
});
if (existingItems.length !== items.length) {
return res.status(404).json(errorResponse('Bazı öğeler bulunamadı'));
}
let updateData = {};
let activityType = '';
let activityDescription = '';
switch (action) {
case 'purchase':
updateData = {
isPurchased: true,
purchasedAt: new Date(),
purchasedById: userId
};
activityType = 'ITEMS_PURCHASED';
activityDescription = `${req.user.firstName} ${req.user.lastName} ${existingItems.length} öğeyi satın aldı`;
break;
case 'unpurchase':
updateData = {
isPurchased: false,
purchasedAt: null,
purchasedBy: null
};
activityType = 'ITEMS_UNPURCHASED';
activityDescription = `${req.user.firstName} ${req.user.lastName} ${existingItems.length} öğenin satın alma durumunu iptal etti`;
break;
case 'delete':
activityType = 'ITEMS_REMOVED';
activityDescription = `${req.user.firstName} ${req.user.lastName} ${existingItems.length} öğeyi listeden kaldırdı`;
break;
}
// Toplu güncelleme veya silme
if (action === 'delete') {
await prisma.listItem.deleteMany({
where: {
id: { in: items },
listId
}
});
} else {
await prisma.listItem.updateMany({
where: {
id: { in: items },
listId
},
data: updateData
});
}
// Liste güncelleme tarihini güncelle
await prisma.shoppingList.update({
where: { id: listId },
data: { updatedAt: new Date() }
});
// Aktivite kaydı oluştur
await prisma.activity.create({
data: {
type: activityType,
description: activityDescription,
userId,
listId
}
});
// Liste üyelerine bildirim gönder
await notificationService.notifyListMembers(
listId,
userId,
{
type: activityType,
message: activityDescription,
data: { itemCount: existingItems.length }
}
);
// Socket.IO ile gerçek zamanlı güncelleme
const io = req.app.get('io');
if (io) {
io.to(`list_${listId}`).emit('itemsBulkUpdated', {
items: items,
action: action,
updatedBy: req.user
});
}
res.json(successResponse(`${existingItems.length} öğe başarıyla ${action === 'purchase' ? 'satın alındı' : action === 'unpurchase' ? 'satın alma iptal edildi' : 'silindi'}`));
})
);
module.exports = router;