full app
This commit is contained in:
175
controllers/panel/ads/adsPanelController.js
Normal file
175
controllers/panel/ads/adsPanelController.js
Normal file
@@ -0,0 +1,175 @@
|
||||
const AdModel = require('../../../models/AdModel')
|
||||
const AdPricingRuleModel = require('../../../models/AdPricingRuleModel')
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
const { ProvinceModel, CityModel, CountryModel } = require('../../../models/StateCity')
|
||||
|
||||
const AD_TYPES = AdModel.AD_TYPES
|
||||
|
||||
/** GET /panel/ads — تبِ اول: لیستِ تبلیغهای ثبتشده */
|
||||
const listAds = async (req, res, next) => {
|
||||
try {
|
||||
const { page = 1, limit = 20, status, ad_type: adType } = req.query
|
||||
const filter = {}
|
||||
if (status) filter.status = status
|
||||
if (adType) filter.ad_type = adType
|
||||
|
||||
const [ads, totalItems] = await Promise.all([
|
||||
AdModel.find(filter)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip((Number(page) - 1) * Number(limit))
|
||||
.limit(Number(limit))
|
||||
.lean(),
|
||||
AdModel.countDocuments(filter)
|
||||
])
|
||||
|
||||
const userIds = [...new Set(ads.map((a) => String(a.user_id)))]
|
||||
const users = await UserModel.find({ _id: { $in: userIds } })
|
||||
.select('user_name first_name last_name profile_image')
|
||||
.lean()
|
||||
const usersById = Object.fromEntries(users.map((u) => [String(u._id), u]))
|
||||
|
||||
const now = Date.now()
|
||||
const rows = ads.map((ad) => ({
|
||||
...ad,
|
||||
advertiser: usersById[String(ad.user_id)] || null,
|
||||
effective_status:
|
||||
ad.status === 'active' && ad.expires_at && new Date(ad.expires_at).getTime() < now
|
||||
? 'expired'
|
||||
: ad.status
|
||||
}))
|
||||
|
||||
return res.json({
|
||||
ads: rows,
|
||||
totalItems,
|
||||
totalPages: Math.ceil(totalItems / Number(limit)) || 1
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** GET /panel/ads/:id — جزئیات کامل + آنالیتیکس یه تبلیغ */
|
||||
const getAdDetailPanel = async (req, res, next) => {
|
||||
try {
|
||||
const ad = await AdModel.findById(req.params.id).lean()
|
||||
if (!ad) {
|
||||
return res.status(404).json({ error: true, message: 'تبلیغ یافت نشد' })
|
||||
}
|
||||
|
||||
const [advertiser, country, province, city] = await Promise.all([
|
||||
UserModel.findById(ad.user_id).select('user_name first_name last_name profile_image').lean(),
|
||||
ad.target_country_id != null ? CountryModel.findOne({ id: ad.target_country_id }).lean() : null,
|
||||
ad.target_province_id != null ? ProvinceModel.findOne({ id: ad.target_province_id }).lean() : null,
|
||||
ad.target_city_id != null ? CityModel.findOne({ id: ad.target_city_id }).lean() : null
|
||||
])
|
||||
|
||||
const now = Date.now()
|
||||
const effectiveStatus =
|
||||
ad.status === 'active' && ad.expires_at && new Date(ad.expires_at).getTime() < now
|
||||
? 'expired'
|
||||
: ad.status
|
||||
|
||||
return res.json({
|
||||
ad: {
|
||||
...ad,
|
||||
effective_status: effectiveStatus,
|
||||
advertiser,
|
||||
target_country_name: country?.name || null,
|
||||
target_province_name: province?.name || null,
|
||||
target_city_name: city?.name || null,
|
||||
click_through_rate: ad.views_count > 0 ? Number(((ad.clicks_count / ad.views_count) * 100).toFixed(2)) : 0
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** GET /panel/ads/pricing — تبِ دوم: لیستِ قانونهای قیمت */
|
||||
const listPricingRules = async (req, res, next) => {
|
||||
try {
|
||||
const rules = await AdPricingRuleModel.find({}).sort({ createdAt: -1 }).lean()
|
||||
const provinceIds = [...new Set(rules.map((r) => r.province_id).filter((v) => v != null))]
|
||||
const provinces = provinceIds.length
|
||||
? await ProvinceModel.find({ id: { $in: provinceIds } }).lean()
|
||||
: []
|
||||
const provincesById = Object.fromEntries(provinces.map((p) => [p.id, p.name]))
|
||||
|
||||
const rows = rules.map((r) => ({
|
||||
...r,
|
||||
province_name: r.province_id != null ? provincesById[r.province_id] || null : null
|
||||
}))
|
||||
|
||||
return res.json({ rules: rows })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /panel/ads/pricing — افزودنِ قانونِ قیمتِ یه سناریوی خاص */
|
||||
const createPricingRule = async (req, res, next) => {
|
||||
try {
|
||||
const { ad_type: adType, province_id: provinceId, gender, expertise, sub_expertise: subExpertise, price } = req.body
|
||||
if (!AD_TYPES.includes(adType)) {
|
||||
return res.status(422).json({ error: true, message: 'نوع تبلیغ نامعتبر است' })
|
||||
}
|
||||
if (!price || Number(price) <= 0) {
|
||||
return res.status(422).json({ error: true, message: 'قیمت نامعتبر است' })
|
||||
}
|
||||
if (gender && !['female', 'male'].includes(gender)) {
|
||||
return res.status(422).json({ error: true, message: 'جنسیت نامعتبر است' })
|
||||
}
|
||||
|
||||
const rule = await AdPricingRuleModel.create({
|
||||
ad_type: adType,
|
||||
province_id: provinceId != null && provinceId !== '' ? Number(provinceId) : null,
|
||||
gender: gender || null,
|
||||
expertise: expertise ? String(expertise).trim() : null,
|
||||
sub_expertise: subExpertise ? String(subExpertise).trim() : null,
|
||||
price: Number(price)
|
||||
})
|
||||
|
||||
return res.status(201).json({ rule })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** PATCH /panel/ads/pricing/:id */
|
||||
const updatePricingRule = async (req, res, next) => {
|
||||
try {
|
||||
const rule = await AdPricingRuleModel.findById(req.params.id)
|
||||
if (!rule) {
|
||||
return res.status(404).json({ error: true, message: 'قانون یافت نشد' })
|
||||
}
|
||||
const { price, gender, expertise, sub_expertise: subExpertise, province_id: provinceId } = req.body
|
||||
if (price !== undefined) rule.price = Number(price)
|
||||
if (gender !== undefined) rule.gender = gender || null
|
||||
if (expertise !== undefined) rule.expertise = expertise ? String(expertise).trim() : null
|
||||
if (subExpertise !== undefined) rule.sub_expertise = subExpertise ? String(subExpertise).trim() : null
|
||||
if (provinceId !== undefined) rule.province_id = provinceId != null && provinceId !== '' ? Number(provinceId) : null
|
||||
await rule.save()
|
||||
return res.json({ rule })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** DELETE /panel/ads/pricing/:id */
|
||||
const deletePricingRule = async (req, res, next) => {
|
||||
try {
|
||||
await AdPricingRuleModel.deleteOne({ _id: req.params.id })
|
||||
return res.json({ success: true })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listAds,
|
||||
getAdDetailPanel,
|
||||
listPricingRules,
|
||||
createPricingRule,
|
||||
updatePricingRule,
|
||||
deletePricingRule
|
||||
}
|
||||
@@ -3,8 +3,12 @@ const NotificationModel = require('../../../models/NotificationModel')
|
||||
const UserModel = require('../../../models/UserModel')
|
||||
const jMoment = require('moment-jalaali')
|
||||
const { sendPushToTokens } = require('../../../utils/pushNotification')
|
||||
const { buildStorageUrl } = require('../../../utils/storageUrl')
|
||||
|
||||
/** ارسال اعلان همگانی — مثل ارسال پیامک، به همهی کاربران */
|
||||
/** ارسال اعلان همگانی — مثل ارسال پیامک، به همهی کاربران. بنر اختیاریه (multer:
|
||||
* routes/panel/notifications/index.js) — هم توی خودِ پوشِ FCM (notification.image،
|
||||
* از قبل توی utils/pushNotification.js پشتیبانی میشد) و هم توی ردیفِ دیتابیسِ هر
|
||||
* کاربر (برای لیستِ داخلِ اپ) ذخیره میشه */
|
||||
const createBroadcast = async (req, res, next) => {
|
||||
try {
|
||||
const { title, body } = req.body
|
||||
@@ -12,11 +16,14 @@ const createBroadcast = async (req, res, next) => {
|
||||
return res.status(400).json({ message: 'عنوان و متن اعلان الزامی است' })
|
||||
}
|
||||
|
||||
const image = req.file ? buildStorageUrl(`/broadcasts/${req.file.filename}`) : null
|
||||
|
||||
const users = await UserModel.find({}).select('_id fcm_tokens fcm_web_tokens').lean()
|
||||
|
||||
const broadcast = await AdminBroadcastModel.create({
|
||||
title,
|
||||
body,
|
||||
image,
|
||||
recipientCount: users.length,
|
||||
sentBy: req.admin?._id
|
||||
})
|
||||
@@ -28,6 +35,7 @@ const createBroadcast = async (req, res, next) => {
|
||||
type: 'admin_broadcast',
|
||||
title,
|
||||
description: body,
|
||||
image,
|
||||
broadcast_id: broadcast._id
|
||||
}))
|
||||
)
|
||||
@@ -39,7 +47,8 @@ const createBroadcast = async (req, res, next) => {
|
||||
title,
|
||||
description: body,
|
||||
type: 'admin_broadcast',
|
||||
data: { broadcastId: String(broadcast._id) }
|
||||
data: { broadcastId: String(broadcast._id) },
|
||||
image
|
||||
}).catch((err) => console.error('[notificationsController] ارسال پوش همگانی خطا:', err.message))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user