hMarket Trae ilk versiyon

This commit is contained in:
hOLOlu
2026-02-03 01:22:08 +03:00
commit 2b861156fe
74 changed files with 42127 additions and 0 deletions

653
backend/src/routes/items.js Normal file
View File

@@ -0,0 +1,653 @@
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;