653 lines
19 KiB
JavaScript
653 lines
19 KiB
JavaScript
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; |