feat(dashboard): add aggregated dashboard stats API, robust logout, and warehouse seeders
deploy-chabokan / deploy (push) Canceled after 0s
deploy-chabokan / deploy (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
const catchAsync = require('../../globalErrorHandler/catchAsync')
|
||||||
|
const dashboardService = require('./dashboardService')
|
||||||
|
|
||||||
|
exports.getDashboardStats = catchAsync(async (req, res, next) => {
|
||||||
|
const stats = await dashboardService.getDashboardStats()
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'success',
|
||||||
|
data: stats,
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const dashboardController = require('./dashboardController')
|
||||||
|
const loginCheck = require('../../middlewares/loginCheck')
|
||||||
|
|
||||||
|
const router = new express.Router()
|
||||||
|
|
||||||
|
router.use(loginCheck.loginProtect)
|
||||||
|
|
||||||
|
router.get('/stats', dashboardController.getDashboardStats)
|
||||||
|
router.get('/summary', dashboardController.getDashboardStats)
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ const runSeeder = async () => {
|
|||||||
status: availableStatus?._id,
|
status: availableStatus?._id,
|
||||||
nationalGeoCode: '537123395123439',
|
nationalGeoCode: '537123395123439',
|
||||||
description: 'مجتمع اداری ۱۰ طبقه با دسترسی عالی به بزرگراه صدر و مترو',
|
description: 'مجتمع اداری ۱۰ طبقه با دسترسی عالی به بزرگراه صدر و مترو',
|
||||||
|
area: 1500,
|
||||||
dong: 6,
|
dong: 6,
|
||||||
ownerName: 'شرکت سرمایهگذاری پارس',
|
ownerName: 'شرکت سرمایهگذاری پارس',
|
||||||
registrationPlate: '۱۲۳۴/۵۶',
|
registrationPlate: '۱۲۳۴/۵۶',
|
||||||
@@ -42,6 +43,7 @@ const runSeeder = async () => {
|
|||||||
status: availableStatus?._id,
|
status: availableStatus?._id,
|
||||||
nationalGeoCode: '536987394852139',
|
nationalGeoCode: '536987394852139',
|
||||||
description: 'زمین دو کله با بر ۲۵ متر و دستور نقشه ۵ طبقه مسکونی',
|
description: 'زمین دو کله با بر ۲۵ متر و دستور نقشه ۵ طبقه مسکونی',
|
||||||
|
area: 600,
|
||||||
dong: 6,
|
dong: 6,
|
||||||
ownerName: 'محسن رضایی',
|
ownerName: 'محسن رضایی',
|
||||||
registrationPlate: '۴۵۶۷/۸۹',
|
registrationPlate: '۴۵۶۷/۸۹',
|
||||||
@@ -64,6 +66,7 @@ const runSeeder = async () => {
|
|||||||
status: reservedStatus?._id || availableStatus?._id,
|
status: reservedStatus?._id || availableStatus?._id,
|
||||||
nationalGeoCode: '539145395412039',
|
nationalGeoCode: '539145395412039',
|
||||||
description: 'واحد ۳۰۰ متری طبقه ۸ با ویوی ابدی کاخ نیاوران و ۳ پارکینگ سندی',
|
description: 'واحد ۳۰۰ متری طبقه ۸ با ویوی ابدی کاخ نیاوران و ۳ پارکینگ سندی',
|
||||||
|
area: 300,
|
||||||
dong: 6,
|
dong: 6,
|
||||||
ownerName: 'سارا صادقی',
|
ownerName: 'سارا صادقی',
|
||||||
registrationPlate: '۷۸۹۰/۱۲',
|
registrationPlate: '۷۸۹۰/۱۲',
|
||||||
@@ -86,6 +89,7 @@ const runSeeder = async () => {
|
|||||||
status: availableStatus?._id,
|
status: availableStatus?._id,
|
||||||
nationalGeoCode: '645123328456139',
|
nationalGeoCode: '645123328456139',
|
||||||
description: 'باغ ۲۰۰۰ متری با درختان کهنسال و عمارت دوبلکس ۴۰۰ متری',
|
description: 'باغ ۲۰۰۰ متری با درختان کهنسال و عمارت دوبلکس ۴۰۰ متری',
|
||||||
|
area: 2000,
|
||||||
dong: 6,
|
dong: 6,
|
||||||
ownerName: 'علیرضا زندی',
|
ownerName: 'علیرضا زندی',
|
||||||
registrationPlate: '۳۲۱۴/۵۶',
|
registrationPlate: '۳۲۱۴/۵۶',
|
||||||
@@ -108,6 +112,7 @@ const runSeeder = async () => {
|
|||||||
status: inReviewStatus?._id || availableStatus?._id,
|
status: inReviewStatus?._id || availableStatus?._id,
|
||||||
nationalGeoCode: '584123361456239',
|
nationalGeoCode: '584123361456239',
|
||||||
description: 'مغازه تجاری دو دهنه بر خیابان اصلی چهارباغ با سند شش دانگ ملکیت',
|
description: 'مغازه تجاری دو دهنه بر خیابان اصلی چهارباغ با سند شش دانگ ملکیت',
|
||||||
|
area: 180,
|
||||||
dong: 6,
|
dong: 6,
|
||||||
ownerName: 'محمد مهدی اصفهانی',
|
ownerName: 'محمد مهدی اصفهانی',
|
||||||
registrationPlate: '۹۶۳۲/۱۴',
|
registrationPlate: '۹۶۳۲/۱۴',
|
||||||
|
|||||||
@@ -39,20 +39,26 @@ exports.loginUser = catchAsync(async (req, res, next) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
exports.logoutUser = catchAsync(async (req, res, next) => {
|
exports.logoutUser = catchAsync(async (req, res, next) => {
|
||||||
res.cookie('belonging_token', '', {
|
const isProduction = process.env.NODE_ENV === 'production'
|
||||||
httpOnly: true,
|
|
||||||
maxAge: 0,
|
// Clear default host cookies
|
||||||
path: '/',
|
res.clearCookie('belonging_token', { path: '/' })
|
||||||
})
|
res.clearCookie('bm-token', { path: '/' })
|
||||||
res.cookie('bm-token', '', {
|
res.clearCookie('jwt', { path: '/' })
|
||||||
httpOnly: false,
|
|
||||||
maxAge: 0,
|
// Clear domain cookies if in production or mbmanager domain
|
||||||
path: '/',
|
if (isProduction || req.hostname.includes('mbmanager.ir')) {
|
||||||
})
|
res.clearCookie('belonging_token', { domain: '.mbmanager.ir', path: '/' })
|
||||||
|
res.clearCookie('bm-token', { domain: '.mbmanager.ir', path: '/' })
|
||||||
|
res.clearCookie('jwt', { domain: '.mbmanager.ir', path: '/' })
|
||||||
|
}
|
||||||
|
|
||||||
|
res.cookie('belonging_token', '', { maxAge: 0, expires: new Date(0), path: '/' })
|
||||||
|
res.cookie('bm-token', '', { maxAge: 0, expires: new Date(0), path: '/' })
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
message: 'کاربر خارج شد',
|
message: 'کاربر با موفقیت خارج شد',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
const Warehouse = require('./warehouseModel')
|
||||||
|
const WarehouseItem = require('../warehouseItem/warehouseItemModel')
|
||||||
|
const WarehouseItemTransaction = require('../warehouseItem/warehouseItemTransactionModel')
|
||||||
|
const WarehouseItemStock = require('../warehouseItem/warehouseItemStockModel')
|
||||||
|
const User = require('../user/userModel')
|
||||||
|
const chalk = require('chalk')
|
||||||
|
|
||||||
|
const runSeeder = async () => {
|
||||||
|
// 1. Find or create admin user for actor
|
||||||
|
const adminUser = await User.findOne({ email: 'superadmin@mbmanager.ir' }) || await User.findOne()
|
||||||
|
|
||||||
|
// 2. Warehouses
|
||||||
|
const sampleWarehouses = [
|
||||||
|
{
|
||||||
|
name: 'انبار مرکزی تهران',
|
||||||
|
code: 'WH-TEH-01',
|
||||||
|
location: 'تهران، جاده مخصوص کرج، کیلومتر ۱۱',
|
||||||
|
description: 'انبار اصلی مصالح ساختمانی و تجهیزات مکانیکی',
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'انبار پروژه نیاوران',
|
||||||
|
code: 'WH-NYA-02',
|
||||||
|
location: 'تهران، نیاوران، خیابان باهنر',
|
||||||
|
description: 'انبار پای کار پروژه برج یاس',
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'انبار مصالح اصفهان',
|
||||||
|
code: 'WH-ISF-01',
|
||||||
|
location: 'اصفهان، شهرک صنعتی جی',
|
||||||
|
description: 'انبار پشتیبانی پروژههای مرکزی',
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const createdWarehouses = []
|
||||||
|
for (const wh of sampleWarehouses) {
|
||||||
|
let doc = await Warehouse.findOne({ name: wh.name })
|
||||||
|
if (!doc) {
|
||||||
|
doc = await Warehouse.create(wh)
|
||||||
|
}
|
||||||
|
createdWarehouses.push(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Warehouse Items
|
||||||
|
const sampleItems = [
|
||||||
|
{
|
||||||
|
nameFa: 'سیمان تیپ ۲ (کیسه ۵۰ کیلویی)',
|
||||||
|
nameEn: 'Cement Type 2 (50kg)',
|
||||||
|
code: 'CEM-T2-50',
|
||||||
|
unitFa: 'کیسه',
|
||||||
|
unitEn: 'Bag',
|
||||||
|
category: 'مصالح پایه',
|
||||||
|
description: 'سیمان پرتلند تیپ ۲ استاندارد',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nameFa: 'میلگرد آجدار سایز ۱۶ (A3)',
|
||||||
|
nameEn: 'Rebar A3 Size 16',
|
||||||
|
code: 'REB-16-A3',
|
||||||
|
unitFa: 'شاخه',
|
||||||
|
unitEn: 'Piece',
|
||||||
|
category: 'آهن آلات',
|
||||||
|
description: 'میلگرد شاخه ۱۲ متری فولاد مبارکه',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nameFa: 'سرامیک پرسلان ۶۰*۱۲۰',
|
||||||
|
nameEn: 'Porcelain Tile 60x120',
|
||||||
|
code: 'TIL-60120-P',
|
||||||
|
unitFa: 'مترمربع',
|
||||||
|
unitEn: 'Sqm',
|
||||||
|
category: 'نازککاری',
|
||||||
|
description: 'سرامیک درجه یک کالیبره مات',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nameFa: 'لوله پلیاتیلن سایز ۶۳',
|
||||||
|
nameEn: 'PE Pipe Size 63',
|
||||||
|
code: 'PIP-PE-63',
|
||||||
|
unitFa: 'متر',
|
||||||
|
unitEn: 'Meter',
|
||||||
|
category: 'تأسیسات مکانیکی',
|
||||||
|
description: 'لوله آبرسانی ۱۰ بار استاندارد',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nameFa: 'رنگ اکریلیک پایه آب (حلب ۲۰ لیتری)',
|
||||||
|
nameEn: 'Acrylic Paint 20L',
|
||||||
|
code: 'PNT-ACR-20L',
|
||||||
|
unitFa: 'حلب',
|
||||||
|
unitEn: 'Can',
|
||||||
|
category: 'رنگ و نقاشی',
|
||||||
|
description: 'رنگ مات قابل شستشو درجه یک',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const createdItems = []
|
||||||
|
for (const it of sampleItems) {
|
||||||
|
let doc = await WarehouseItem.findOne({ code: it.code })
|
||||||
|
if (!doc) {
|
||||||
|
doc = await WarehouseItem.create(it)
|
||||||
|
}
|
||||||
|
createdItems.push(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Sample Transactions
|
||||||
|
const txCount = await WarehouseItemTransaction.countDocuments()
|
||||||
|
if (txCount === 0 && adminUser && createdWarehouses.length > 0 && createdItems.length > 0) {
|
||||||
|
const sampleTransactions = [
|
||||||
|
{
|
||||||
|
item: createdItems[0]._id,
|
||||||
|
warehouse: createdWarehouses[0]._id,
|
||||||
|
type: 'IN',
|
||||||
|
quantity: 500,
|
||||||
|
unit: createdItems[0].unitFa,
|
||||||
|
reason: 'خرید مستقیم از کارخانه سیمان تهران',
|
||||||
|
actor: adminUser._id,
|
||||||
|
actorName: adminUser.name || 'مدیر سیستم',
|
||||||
|
note: 'بارگیری از کارخانه با فاکتور شماره ۱۲۸۹',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: createdItems[1]._id,
|
||||||
|
warehouse: createdWarehouses[0]._id,
|
||||||
|
type: 'IN',
|
||||||
|
quantity: 120,
|
||||||
|
unit: createdItems[1].unitFa,
|
||||||
|
reason: 'تأمین آهنآلات اسکلت سازه',
|
||||||
|
actor: adminUser._id,
|
||||||
|
actorName: adminUser.name || 'مدیر سیستم',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: createdItems[0]._id,
|
||||||
|
warehouse: createdWarehouses[1]._id,
|
||||||
|
type: 'OUT',
|
||||||
|
quantity: 80,
|
||||||
|
unit: createdItems[0].unitFa,
|
||||||
|
reason: 'مصرف در بتنریزی سقف طبقه پنجم',
|
||||||
|
actor: adminUser._id,
|
||||||
|
actorName: adminUser.name || 'مدیر سیستم',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: createdItems[2]._id,
|
||||||
|
warehouse: createdWarehouses[0]._id,
|
||||||
|
type: 'IN',
|
||||||
|
quantity: 350,
|
||||||
|
unit: createdItems[2].unitFa,
|
||||||
|
reason: 'خرید مصالح نازککاری',
|
||||||
|
actor: adminUser._id,
|
||||||
|
actorName: adminUser.name || 'مدیر سیستم',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: createdItems[3]._id,
|
||||||
|
warehouse: createdWarehouses[1]._id,
|
||||||
|
type: 'IN',
|
||||||
|
quantity: 200,
|
||||||
|
unit: createdItems[3].unitFa,
|
||||||
|
reason: 'تجهیز تأسیسات لولهکشی آب',
|
||||||
|
actor: adminUser._id,
|
||||||
|
actorName: adminUser.name || 'مدیر سیستم',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const tx of sampleTransactions) {
|
||||||
|
await WarehouseItemTransaction.create(tx)
|
||||||
|
|
||||||
|
// Update or create stock
|
||||||
|
const delta = tx.type === 'IN' ? tx.quantity : -tx.quantity
|
||||||
|
await WarehouseItemStock.findOneAndUpdate(
|
||||||
|
{ item: tx.item, warehouse: tx.warehouse },
|
||||||
|
{ $inc: { quantity: delta } },
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
console.log(chalk.green.bold(`WAREHOUSE: Created ${sampleTransactions.length} sample inventory transactions`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runSeeder }
|
||||||
@@ -4,6 +4,7 @@ const roleSeeder = require('../components/role/roleSeeder')
|
|||||||
const featureSeeder = require('../components/feature/featureSeeder')
|
const featureSeeder = require('../components/feature/featureSeeder')
|
||||||
const propertyStatusSeeder = require('../components/propertyStatus/propertyStatusSeeder')
|
const propertyStatusSeeder = require('../components/propertyStatus/propertyStatusSeeder')
|
||||||
const propertySeeder = require('../components/property/propertySeeder')
|
const propertySeeder = require('../components/property/propertySeeder')
|
||||||
|
const warehouseSeeder = require('../components/warehouse/warehouseSeeder')
|
||||||
const propertyService = require('../components/property/propertyService')
|
const propertyService = require('../components/property/propertyService')
|
||||||
const transactionService = require('../components/transaction/transactionService')
|
const transactionService = require('../components/transaction/transactionService')
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ module.exports.run = async () => {
|
|||||||
await featureSeeder.runSeeder()
|
await featureSeeder.runSeeder()
|
||||||
await propertyStatusSeeder.runSeeder()
|
await propertyStatusSeeder.runSeeder()
|
||||||
await propertySeeder.runSeeder()
|
await propertySeeder.runSeeder()
|
||||||
|
await warehouseSeeder.runSeeder()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await propertyService.indexAllProperties()
|
await propertyService.indexAllProperties()
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ const featureRoutes = require('../components/feature/featureRoutes')
|
|||||||
const warehouseItemRoutes = require('../components/warehouseItem/warehouseItemRoutes')
|
const warehouseItemRoutes = require('../components/warehouseItem/warehouseItemRoutes')
|
||||||
const warehouseRoutes = require('../components/warehouse/warehouseRoutes')
|
const warehouseRoutes = require('../components/warehouse/warehouseRoutes')
|
||||||
const settingRoutes = require('../components/setting/settingRoutes')
|
const settingRoutes = require('../components/setting/settingRoutes')
|
||||||
|
const dashboardRoutes = require('../components/dashboard/dashboardRoutes')
|
||||||
|
|
||||||
const routerEndpoints = new express.Router()
|
const routerEndpoints = new express.Router()
|
||||||
|
|
||||||
|
routerEndpoints.use('/dashboard', dashboardRoutes)
|
||||||
routerEndpoints.use('/users', userRoutes)
|
routerEndpoints.use('/users', userRoutes)
|
||||||
routerEndpoints.use('/permissions', permissionRoutes)
|
routerEndpoints.use('/permissions', permissionRoutes)
|
||||||
routerEndpoints.use('/properties', propertyRoutes)
|
routerEndpoints.use('/properties', propertyRoutes)
|
||||||
|
|||||||
Reference in New Issue
Block a user