diff --git a/services/reportService.js b/services/reportService.js new file mode 100644 index 0000000..88eb904 --- /dev/null +++ b/services/reportService.js @@ -0,0 +1,146 @@ +'use strict'; + +/** + * reportService.js + * Rapor hesaplama motoru — kategori bazlı raporlar, stok değerleri ve hareket özetleri. + * Saf fonksiyonlar; test edilebilir, route'tan bağımsız. + */ + +const productModel = require('../models/productModel'); +const stockModel = require('../models/stockModel'); +const categoryModel = require('../models/categoryModel'); + +/** ISO tarihini Date nesnesine güvenle çevirir (geçersizse boş aralık dönmez). */ +function toDate(value) { + const d = value ? new Date(value) : null; + return d && !isNaN(d.getTime()) ? d : null; +} + +/** + * Belirli bir kategoriye ait ürünlerin toplam stok değerini hesaplar. + * @param {number} categoryId kategori ID + * @returns {number} toplam değer (stock * unitPrice) + */ +function valueByCategory(categoryId) { + const categories = categoryModel.readAll(); + const catId = Number(categoryId); + + // id:0 / 'all' => tüm kategoriler + if (catId === 0 || String(categoryId).toLowerCase() === 'all') { + const products = productModel.readAll(); + return products.reduce((sum, p) => sum + Number(p.stock) * Number(p.unitPrice), 0); + } + + const cat = categories.find((c) => c.id === catId); + if (!cat) return 0; + + const products = productModel.findByCategory(catId); + return products.reduce((sum, p) => sum + Number(p.stock) * Number(p.unitPrice), 0); +} + +/** + * Tarih aralığında kategori ürünlerine ait giriş/çıkış özeti. + * @param {number|null} categoryId kategori ID (0/null = hepsi) + * @param {string} fromDate başlangıç tarihi (ISO) + * @param {string} toDate bitiş tarihi (ISO) + * @returns {Object} {in, out, net} + */ +function movementSummary(categoryId = null, fromDate = '', toDate = '') { + const from = toDate(fromDate); + const to = toDate(toDate); + + // Kategoriye ait ürün ID'lerini bul + let productIds = new Set(); + const catId = Number(categoryId); + if (catId && !isNaN(catId) && catId !== 0) { + productModel.findByCategory(catId).forEach((p) => productIds.add(Number(p.id))); + } else { + productModel.readAll().forEach((p) => productIds.add(Number(p.id))); + } + + const movements = stockModel.readAll().filter((m) => { + if (!productIds.has(Number(m.productId))) return false; + const d = new Date(m.date); + if (from && d < from) return false; + if (to) { + // to bitiş gününü kapsar + const toEnd = new Date(to); + toEnd.setHours(23, 59, 59, 999); + if (d > toEnd) return false; + } + return true; + }); + + let inQty = 0; + let outQty = 0; + for (const m of movements) { + if (m.type === 'in') inQty += Number(m.quantity) || 0; + else if (m.type === 'out') outQty += Number(m.quantity) || 0; + } + + return { in: inQty, out: outQty, net: inQty - outQty, movements }; +} + +/** + * Kategori bazlı tam rapor. + * @param {number|null} categoryId kategori ID + * @param {Object} opts {fromDate, toDate} + * @returns {Object} {category, totalProducts, totalStockValue, incomingQty, outgoingQty, movements, lowStockCount} + */ +function categoryReport(categoryId = null, opts = {}) { + const { fromDate = '', toDate = '' } = opts || {}; + const catId = Number(categoryId); + const categories = categoryModel.readAll(); + const category = + (catId && categories.find((c) => c.id === catId)) || + (String(categoryId).toLowerCase() === 'all' ? { id: 0, name: 'Tümü' } : null); + + const products = category && category.id !== 0 + ? productModel.findByCategory(catId) + : productModel.readAll(); + + const totalStockValue = products.reduce((s, p) => s + Number(p.stock) * Number(p.unitPrice), 0); + const summary = movementSummary(catId || 0, fromDate, toDate); + + return { + category, + totalProducts: products.length, + totalStockValue, + incomingQty: summary.in, + outgoingQty: summary.out, + netQty: summary.net, + movements: summary.movements, + lowStockCount: products.filter( + (p) => Number(p.stock) <= (Number(p.minimumStock) || 10) + ).length + }; +} + +/** + * Tüm kategorilerin özet raporları (dashboard / rapor sayfası tablosu). + * @returns {Array} [{id, name, totalProducts, totalStockValue, lowStockCount}] + */ +function allCategorySummary() { + const categories = categoryModel.readAll(); + return categories.map((cat) => { + const products = productModel.findByCategory(cat.id); + const totalStockValue = products.reduce((s, p) => s + Number(p.stock) * Number(p.unitPrice), 0); + return { + id: cat.id, + name: cat.name, + slug: cat.slug, + totalProducts: products.length, + totalStockValue, + lowStockCount: products.filter( + (p) => Number(p.stock) <= (Number(p.minimumStock) || 10) + ).length + }; + }); +} + +module.exports = { + categoryReport, + valueByCategory, + movementSummary, + allCategorySummary +};