envanter-dokuman/services/reportService.js

147 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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