const express = require('express'); const { body, validationResult, param } = require('express-validator'); const { asyncHandler, formatValidationErrors } = require('../middleware/errorHandler'); const { authenticateToken, checkListMembership, requireListEditPermission } = require('../middleware/auth'); const { validateListCreation, validateListUpdate, validateListItemCreation, validateListItemUpdate, validateUUIDParam, validateCuidParam, validateAddListMember, validatePagination } = require('../utils/validators'); const router = express.Router(); /** * Kullanıcının listelerini getir * GET /api/lists */ router.get('/', authenticateToken, asyncHandler(async (req, res) => { const { page = 1, limit = 10, search } = req.query; const skip = (page - 1) * limit; // Arama koşulları const whereCondition = { OR: [ { ownerId: req.user.id }, { members: { some: { userId: req.user.id } } } ], isActive: true }; if (search) { whereCondition.name = { contains: search, mode: 'insensitive' }; } const [lists, totalCount] = await Promise.all([ req.prisma.shoppingList.findMany({ where: whereCondition, include: { owner: { select: { id: true, username: true, firstName: true, lastName: true, avatar: true } }, members: { include: { user: { select: { id: true, username: true, firstName: true, lastName: true, avatar: true } } } }, items: { select: { id: true, isPurchased: true } }, _count: { select: { items: true, members: true } } }, orderBy: { updatedAt: 'desc' }, skip: parseInt(skip), take: parseInt(limit) }), req.prisma.shoppingList.count({ where: whereCondition }) ]); // Her liste için kullanıcının rolünü belirle ve tamamlanan ürün sayısını hesapla const listsWithUserRole = lists.map(list => { let userRole = 'viewer'; if (list.ownerId === req.user.id) { userRole = 'owner'; } else { const membership = list.members.find(member => member.userId === req.user.id); if (membership) { userRole = membership.role; } } // Tamamlanan ürün sayısını hesapla const completedItems = list.items.filter(item => item.isPurchased).length; // Items'ı response'dan çıkar (sadece count için kullandık) const { items, ...listWithoutItems } = list; return { ...listWithoutItems, userRole, _count: { ...list._count, completedItems } }; }); res.json({ success: true, data: { lists: listsWithUserRole, pagination: { currentPage: parseInt(page), totalPages: Math.ceil(totalCount / limit), totalCount, hasNext: skip + parseInt(limit) < totalCount, hasPrev: page > 1 } } }); })); /** * Yeni liste oluştur * POST /api/lists */ router.post('/', authenticateToken, [ body('name') .isLength({ min: 1, max: 100 }) .withMessage('Liste adı 1-100 karakter arasında olmalı'), body('description') .optional() .isLength({ max: 500 }) .withMessage('Açıklama en fazla 500 karakter olabilir'), body('color') .optional() .matches(/^#[0-9A-F]{6}$/i) .withMessage('Geçerli bir hex renk kodu girin'), body('isShared') .optional() .isBoolean() .withMessage('Paylaşım durumu boolean olmalı') ], asyncHandler(async (req, res) => { // Validation hatalarını kontrol et const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, message: 'Girilen bilgilerde hatalar var', errors: formatValidationErrors(errors) }); } const { name, description, color = '#2196F3' } = req.body; const list = await req.prisma.shoppingList.create({ data: { name, description, color, ownerId: req.user.id }, include: { owner: { select: { id: true, username: true, firstName: true, lastName: true, avatar: true } }, _count: { select: { items: true, members: true } } } }); // Aktivite kaydı oluştur await req.prisma.activity.create({ data: { listId: list.id, userId: req.user.id, action: 'list_created', details: { listName: list.name } } }); // Socket.IO ile gerçek zamanlı bildirim gönder req.io.emit('list_created', { list: { ...list, userRole: 'owner' }, user: req.user }); res.status(201).json({ success: true, message: 'Liste başarıyla oluşturuldu', data: { list: { ...list, userRole: 'owner' } } }); })); /** * Liste detaylarını getir * GET /api/lists/:listId */ router.get('/:listId', [ authenticateToken, validateCuidParam('listId'), checkListMembership ], asyncHandler(async (req, res) => { const { listId } = req.params; const list = await req.prisma.shoppingList.findUnique({ where: { id: listId }, include: { owner: { select: { id: true, username: true, firstName: true, lastName: true, avatar: true } }, members: { include: { user: { select: { id: true, username: true, firstName: true, lastName: true, avatar: true } } } }, items: { include: { product: { include: { category: true } } }, orderBy: [ { isPurchased: 'asc' }, { priority: 'desc' }, { sortOrder: 'asc' }, { createdAt: 'asc' } ] }, activities: { include: { user: { select: { id: true, username: true, firstName: true, lastName: true, avatar: true } } }, orderBy: { createdAt: 'desc' }, take: 20 } } }); res.json({ success: true, data: { list: { ...list, userRole: req.userRole } } }); })); /** * Liste güncelle * PUT /api/lists/:listId */ router.put('/:listId', [ authenticateToken, validateCuidParam('listId'), checkListMembership, requireListEditPermission, body('name') .optional() .isLength({ min: 1, max: 100 }) .withMessage('Liste adı 1-100 karakter arasında olmalı'), body('description') .optional() .isLength({ max: 500 }) .withMessage('Açıklama en fazla 500 karakter olabilir'), body('color') .optional() .matches(/^#[0-9A-F]{6}$/i) .withMessage('Geçerli bir hex renk kodu girin'), body('isShared') .optional() .isBoolean() .withMessage('Paylaşım durumu boolean olmalı') ], asyncHandler(async (req, res) => { // Validation hatalarını kontrol et const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, message: 'Girilen bilgilerde hatalar var', errors: formatValidationErrors(errors) }); } const { listId } = req.params; const { name, description, color, isShared } = req.body; const updateData = {}; if (name !== undefined) updateData.name = name; if (description !== undefined) updateData.description = description; if (color !== undefined) updateData.color = color; if (isShared !== undefined) updateData.isShared = isShared; const updatedList = await req.prisma.shoppingList.update({ where: { id: listId }, data: updateData, include: { owner: { select: { id: true, username: true, firstName: true, lastName: true, avatar: true } }, members: { include: { user: { select: { id: true, username: true, firstName: true, lastName: true, avatar: true } } } }, _count: { select: { items: true, members: true } } } }); // Aktivite kaydı oluştur await req.prisma.activity.create({ data: { listId: listId, userId: req.user.id, action: 'list_updated', details: { changes: updateData } } }); // Socket.IO ile gerçek zamanlı güncelleme gönder req.io.to(`list_${listId}`).emit('list_updated', { list: { ...updatedList, userRole: req.userRole }, user: req.user, changes: updateData }); res.json({ success: true, message: 'Liste başarıyla güncellendi', data: { list: { ...updatedList, userRole: req.userRole } } }); })); /** * Liste sil * DELETE /api/lists/:listId */ router.delete('/:listId', [ authenticateToken, validateCuidParam('listId'), checkListMembership ], asyncHandler(async (req, res) => { const { listId } = req.params; // Sadece liste sahibi silebilir if (req.userRole !== 'owner') { return res.status(403).json({ success: false, message: 'Sadece liste sahibi listeyi silebilir' }); } // Soft delete (isActive = false) await req.prisma.shoppingList.update({ where: { id: listId }, data: { isActive: false } }); // Aktivite kaydı oluştur await req.prisma.activity.create({ data: { listId: listId, userId: req.user.id, action: 'list_deleted', details: { listName: req.list.name } } }); // Socket.IO ile gerçek zamanlı bildirim gönder req.io.to(`list_${listId}`).emit('list_deleted', { listId: listId, user: req.user }); res.json({ success: true, message: 'Liste başarıyla silindi' }); })); /** * Listeye üye ekle * POST /api/lists/:listId/members */ router.post('/:listId/members', [ authenticateToken, validateCuidParam('listId'), checkListMembership, requireListEditPermission, body('email') .isEmail() .normalizeEmail() .withMessage('Geçerli bir e-posta adresi girin'), body('role') .optional() .isIn(['admin', 'member', 'viewer', 'EDITOR', 'VIEWER']) .withMessage('Geçerli bir rol seçin') ], asyncHandler(async (req, res) => { // Validation hatalarını kontrol et const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, message: 'Girilen bilgilerde hatalar var', errors: formatValidationErrors(errors) }); } const { listId } = req.params; let { email, role = 'member' } = req.body; // Role mapping if (role === 'EDITOR') role = 'member'; if (role === 'VIEWER') role = 'viewer'; // Kullanıcıyı bul const targetUser = await req.prisma.user.findUnique({ where: { email, isActive: true }, select: { id: true, email: true, username: true, firstName: true, lastName: true, avatar: true } }); if (!targetUser) { return res.status(404).json({ success: false, message: 'Bu e-posta adresine sahip aktif kullanıcı bulunamadı' }); } // Zaten üye mi kontrol et const existingMember = await req.prisma.listMember.findUnique({ where: { listId_userId: { listId: listId, userId: targetUser.id } } }); if (existingMember) { return res.status(400).json({ success: false, message: 'Bu kullanıcı zaten liste üyesi' }); } // Liste sahibi kendini üye olarak ekleyemez if (targetUser.id === req.list.ownerId) { return res.status(400).json({ success: false, message: 'Liste sahibi zaten tüm yetkilere sahip' }); } // Üye ekle const newMember = await req.prisma.listMember.create({ data: { listId: listId, userId: targetUser.id, role: role }, include: { user: { select: { id: true, email: true, username: true, firstName: true, lastName: true, avatar: true } } } }); // Aktivite kaydı oluştur await req.prisma.activity.create({ data: { listId: listId, userId: req.user.id, action: 'member_added', details: { addedUser: { id: targetUser.id, username: targetUser.username, firstName: targetUser.firstName, lastName: targetUser.lastName }, role: role } } }); // Bildirim oluştur await req.prisma.notification.create({ data: { userId: targetUser.id, title: 'Listeye Eklendi', message: `${req.user.firstName} ${req.user.lastName} sizi "${req.list.name}" listesine ekledi`, type: 'list_shared', data: { listId: listId, listName: req.list.name, invitedBy: { id: req.user.id, username: req.user.username, firstName: req.user.firstName, lastName: req.user.lastName } } } }); // Socket.IO ile gerçek zamanlı bildirim gönder req.io.to(`list_${listId}`).emit('member_added', { member: newMember, user: req.user }); // Yeni üyeye özel bildirim gönder req.io.to(`user_${targetUser.id}`).emit('list_invitation', { listId: listId, listName: req.list.name, invitedBy: req.user }); res.status(201).json({ success: true, message: 'Üye başarıyla eklendi', data: { member: newMember } }); })); /** * Liste üyesini çıkar * DELETE /api/lists/:listId/members/:userId */ router.delete('/:listId/members/:userId', [ authenticateToken, validateCuidParam('listId'), validateCuidParam('userId'), checkListMembership, requireListEditPermission ], asyncHandler(async (req, res) => { const { listId, userId } = req.params; // Üyeliği bul const membership = await req.prisma.listMember.findUnique({ where: { listId_userId: { listId: listId, userId: userId } }, include: { user: { select: { id: true, username: true, firstName: true, lastName: true, avatar: true } } } }); if (!membership) { return res.status(404).json({ success: false, message: 'Üye bulunamadı' }); } // Üyeliği sil await req.prisma.listMember.delete({ where: { listId_userId: { listId: listId, userId: userId } } }); // Aktivite kaydı oluştur await req.prisma.activity.create({ data: { listId: listId, userId: req.user.id, action: 'member_removed', details: { removedUser: { id: membership.user.id, username: membership.user.username, firstName: membership.user.firstName, lastName: membership.user.lastName } } } }); // Socket.IO ile gerçek zamanlı bildirim gönder req.io.to(`list_${listId}`).emit('member_removed', { userId: userId, user: req.user }); res.json({ success: true, message: 'Üye başarıyla çıkarıldı' }); })); module.exports = router;