envanter-dokuman/services/reportService.js

147 lines
4.6 KiB
JavaScript
Raw Normal View History

'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
};