233 lines
8.2 KiB
JavaScript
233 lines
8.2 KiB
JavaScript
const Property = require('../property/propertyModel')
|
|
const Category = require('../category/categoryModel')
|
|
const PropertyStatus = require('../propertyStatus/propertyStatusModel')
|
|
const Warehouse = require('../warehouse/warehouseModel')
|
|
const WarehouseItem = require('../warehouseItem/warehouseItemModel')
|
|
const WarehouseItemTransaction = require('../warehouseItem/warehouseItemTransactionModel')
|
|
const User = require('../user/userModel')
|
|
const Role = require('../role/roleModel')
|
|
const { getSignedUrl } = require('../file/fileService')
|
|
|
|
/**
|
|
* Aggregates live dashboard statistics across properties, warehouses, inventory, and users
|
|
*/
|
|
const getDashboardStats = async () => {
|
|
// 1. Overview counts
|
|
const [
|
|
totalProperties,
|
|
totalAreaResult,
|
|
totalWarehouseItems,
|
|
totalWarehouses,
|
|
totalWarehouseTransactions,
|
|
totalUsers,
|
|
totalCategories,
|
|
totalRoles,
|
|
] = await Promise.all([
|
|
Property.countDocuments({ isActive: { $ne: false } }),
|
|
Property.aggregate([
|
|
{ $match: { isActive: { $ne: false }, area: { $gt: 0 } } },
|
|
{ $group: { _id: null, totalArea: { $sum: '$area' } } },
|
|
]),
|
|
WarehouseItem.countDocuments({ isActive: { $ne: false } }),
|
|
Warehouse.countDocuments(),
|
|
WarehouseItemTransaction.countDocuments(),
|
|
User.countDocuments({ isActive: { $ne: false } }),
|
|
Category.countDocuments(),
|
|
Role.countDocuments(),
|
|
])
|
|
|
|
const totalArea = totalAreaResult?.[0]?.totalArea || 0
|
|
|
|
// 2. Properties grouped by Category
|
|
const propertiesByCategoryRaw = await Property.aggregate([
|
|
{ $match: { isActive: { $ne: false } } },
|
|
{
|
|
$group: {
|
|
_id: '$category',
|
|
count: { $sum: 1 },
|
|
totalArea: { $sum: { $ifNull: ['$area', 0] } },
|
|
},
|
|
},
|
|
{
|
|
$lookup: {
|
|
from: 'categories',
|
|
localField: '_id',
|
|
foreignField: '_id',
|
|
as: 'categoryDoc',
|
|
},
|
|
},
|
|
{ $unwind: { path: '$categoryDoc', preserveNullAndEmptyArrays: true } },
|
|
{ $sort: { count: -1 } },
|
|
])
|
|
|
|
const propertiesByCategory = propertiesByCategoryRaw.map((item) => ({
|
|
categoryId: item._id,
|
|
name: item.categoryDoc?.nativeName || item.categoryDoc?.englishName || 'بدون دستهبندی',
|
|
englishName: item.categoryDoc?.englishName || 'Uncategorized',
|
|
count: item.count,
|
|
totalArea: item.totalArea || 0,
|
|
percentage: totalProperties > 0 ? Math.round((item.count / totalProperties) * 100) : 0,
|
|
}))
|
|
|
|
// 3. Properties grouped by Status
|
|
const propertiesByStatusRaw = await Property.aggregate([
|
|
{ $match: { isActive: { $ne: false } } },
|
|
{
|
|
$group: {
|
|
_id: '$status',
|
|
count: { $sum: 1 },
|
|
},
|
|
},
|
|
{
|
|
$lookup: {
|
|
from: 'propertystatuses',
|
|
localField: '_id',
|
|
foreignField: '_id',
|
|
as: 'statusDoc',
|
|
},
|
|
},
|
|
{ $unwind: { path: '$statusDoc', preserveNullAndEmptyArrays: true } },
|
|
{ $sort: { count: -1 } },
|
|
])
|
|
|
|
const propertiesByStatus = propertiesByStatusRaw.map((item) => ({
|
|
statusId: item._id,
|
|
name: item.statusDoc?.nativeName || item.statusDoc?.englishName || 'نامشخص',
|
|
englishName: item.statusDoc?.englishName || 'Unknown',
|
|
color: item.statusDoc?.color || '#3b82f6',
|
|
count: item.count,
|
|
percentage: totalProperties > 0 ? Math.round((item.count / totalProperties) * 100) : 0,
|
|
}))
|
|
|
|
// 4. Warehouse transactions summary (IN vs OUT)
|
|
const warehouseFlowRaw = await WarehouseItemTransaction.aggregate([
|
|
{
|
|
$group: {
|
|
_id: '$type',
|
|
count: { $sum: 1 },
|
|
totalQuantity: { $sum: '$quantity' },
|
|
},
|
|
},
|
|
])
|
|
|
|
const warehouseFlow = {
|
|
inCount: warehouseFlowRaw.find((f) => f._id === 'IN')?.count || 0,
|
|
outCount: warehouseFlowRaw.find((f) => f._id === 'OUT')?.count || 0,
|
|
inQuantity: warehouseFlowRaw.find((f) => f._id === 'IN')?.totalQuantity || 0,
|
|
outQuantity: warehouseFlowRaw.find((f) => f._id === 'OUT')?.totalQuantity || 0,
|
|
}
|
|
|
|
// 5. Recent registered properties (Last 6)
|
|
const recentPropertiesRaw = await Property.find({ isActive: { $ne: false } })
|
|
.sort({ createdAt: -1 })
|
|
.limit(6)
|
|
.populate('category', 'nativeName englishName')
|
|
.populate('status', 'nativeName englishName color')
|
|
.lean()
|
|
|
|
const recentProperties = await Promise.all(
|
|
recentPropertiesRaw.map(async (prop) => {
|
|
let thumb = null
|
|
if (prop.mainImageUrls && prop.mainImageUrls.length > 0) {
|
|
thumb = await getSignedUrl(prop.mainImageUrls[0], 'properties').catch(() => null)
|
|
}
|
|
return {
|
|
_id: prop._id,
|
|
propertyCode: prop.propertyCode || prop._id.toString().slice(-6),
|
|
name: prop.name || 'ملک بدون عنوان',
|
|
category: prop.category?.nativeName || 'عمومی',
|
|
status: prop.status?.nativeName || 'عادی',
|
|
statusColor: prop.status?.color || '#3b82f6',
|
|
ownerName: prop.ownerName || '—',
|
|
area: prop.area || 0,
|
|
dong: prop.dong || 6,
|
|
imageUrl: thumb || 'https://cloud.mbmanager.ir/images/item-no-image.png',
|
|
registrationPlate: prop.registrationPlate || '—',
|
|
createdAt: prop.createdAt,
|
|
}
|
|
})
|
|
)
|
|
|
|
// 6. Recent warehouse transactions (Last 6)
|
|
const recentWarehouseTransactionsRaw = await WarehouseItemTransaction.find()
|
|
.sort({ createdAt: -1 })
|
|
.limit(6)
|
|
.populate('item', 'nameFa nameEn code unitFa')
|
|
.populate('warehouse', 'name location')
|
|
.lean()
|
|
|
|
const recentWarehouseTransactions = recentWarehouseTransactionsRaw.map((tx) => ({
|
|
_id: tx._id,
|
|
type: tx.type, // 'IN' | 'OUT'
|
|
quantity: tx.quantity,
|
|
unit: tx.unit || tx.item?.unitFa || 'عدد',
|
|
itemName: tx.item?.nameFa || 'کالای نامشخص',
|
|
itemCode: tx.item?.code || '—',
|
|
warehouseName: tx.warehouse?.name || 'انبار مرکزی',
|
|
reason: tx.reason || (tx.type === 'IN' ? 'ورود به انبار' : 'خروج از انبار'),
|
|
actorName: tx.actorName || 'کاربر سیستم',
|
|
date: tx.date || tx.createdAt,
|
|
}))
|
|
|
|
// 7. Monthly Property & Warehouse Trend (Last 6 months)
|
|
const sixMonthsAgo = new Date()
|
|
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 5)
|
|
sixMonthsAgo.setDate(1)
|
|
sixMonthsAgo.setHours(0, 0, 0, 0)
|
|
|
|
const monthlyPropertiesRaw = await Property.aggregate([
|
|
{ $match: { createdAt: { $gte: sixMonthsAgo }, isActive: { $ne: false } } },
|
|
{
|
|
$group: {
|
|
_id: {
|
|
year: { $year: '$createdAt' },
|
|
month: { $month: '$createdAt' },
|
|
},
|
|
count: { $sum: 1 },
|
|
},
|
|
},
|
|
{ $sort: { '_id.year': 1, '_id.month': 1 } },
|
|
])
|
|
|
|
const monthlyWarehouseRaw = await WarehouseItemTransaction.aggregate([
|
|
{ $match: { createdAt: { $gte: sixMonthsAgo } } },
|
|
{
|
|
$group: {
|
|
_id: {
|
|
year: { $year: '$createdAt' },
|
|
month: { $month: '$createdAt' },
|
|
type: '$type',
|
|
},
|
|
count: { $sum: 1 },
|
|
},
|
|
},
|
|
{ $sort: { '_id.year': 1, '_id.month': 1 } },
|
|
])
|
|
|
|
return {
|
|
overview: {
|
|
totalProperties,
|
|
totalArea,
|
|
totalWarehouseItems,
|
|
totalWarehouses,
|
|
totalWarehouseTransactions,
|
|
totalUsers,
|
|
totalCategories,
|
|
totalRoles,
|
|
},
|
|
propertiesByCategory,
|
|
propertiesByStatus,
|
|
warehouseFlow,
|
|
recentProperties,
|
|
recentWarehouseTransactions,
|
|
monthlyTrend: {
|
|
properties: monthlyPropertiesRaw,
|
|
warehouse: monthlyWarehouseRaw,
|
|
},
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
getDashboardStats,
|
|
}
|