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;