- Portfolio CRUD: добавление, редактирование, удаление, переключение публикации - Services CRUD: полное управление услугами с возможностью активации/деактивации - Banner system: новая модель Banner с CRUD операциями и аналитикой кликов - Telegram integration: расширенные настройки бота, обнаружение чатов, отправка сообщений - Media management: улучшенная загрузка файлов с оптимизацией изображений и превью - UI improvements: обновлённые админ-панели с rich-text редактором и drag&drop загрузкой - Database: добавлена таблица banners с полями для баннеров и аналитики
195 lines
4.0 KiB
JavaScript
195 lines
4.0 KiB
JavaScript
const { DataTypes } = require('sequelize');
|
|
const { sequelize } = require('../config/database');
|
|
|
|
const Banner = sequelize.define('Banner', {
|
|
id: {
|
|
type: DataTypes.UUID,
|
|
defaultValue: DataTypes.UUIDV4,
|
|
primaryKey: true
|
|
},
|
|
title: {
|
|
type: DataTypes.STRING,
|
|
allowNull: false,
|
|
validate: {
|
|
notEmpty: true,
|
|
len: [1, 200]
|
|
}
|
|
},
|
|
subtitle: {
|
|
type: DataTypes.STRING,
|
|
allowNull: true,
|
|
validate: {
|
|
len: [0, 300]
|
|
}
|
|
},
|
|
description: {
|
|
type: DataTypes.TEXT,
|
|
allowNull: true
|
|
},
|
|
buttonText: {
|
|
type: DataTypes.STRING,
|
|
allowNull: true,
|
|
validate: {
|
|
len: [0, 50]
|
|
}
|
|
},
|
|
buttonUrl: {
|
|
type: DataTypes.STRING,
|
|
allowNull: true,
|
|
validate: {
|
|
isUrl: true
|
|
}
|
|
},
|
|
image: {
|
|
type: DataTypes.STRING,
|
|
allowNull: true,
|
|
comment: 'Banner background image URL'
|
|
},
|
|
mobileImage: {
|
|
type: DataTypes.STRING,
|
|
allowNull: true,
|
|
comment: 'Mobile-optimized banner image URL'
|
|
},
|
|
position: {
|
|
type: DataTypes.ENUM('hero', 'secondary', 'footer'),
|
|
defaultValue: 'hero',
|
|
allowNull: false
|
|
},
|
|
order: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
allowNull: false,
|
|
comment: 'Display order (lower numbers appear first)'
|
|
},
|
|
isActive: {
|
|
type: DataTypes.BOOLEAN,
|
|
defaultValue: true,
|
|
allowNull: false
|
|
},
|
|
startDate: {
|
|
type: DataTypes.DATE,
|
|
allowNull: true,
|
|
comment: 'Banner start display date'
|
|
},
|
|
endDate: {
|
|
type: DataTypes.DATE,
|
|
allowNull: true,
|
|
comment: 'Banner end display date'
|
|
},
|
|
clickCount: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
allowNull: false
|
|
},
|
|
impressions: {
|
|
type: DataTypes.INTEGER,
|
|
defaultValue: 0,
|
|
allowNull: false
|
|
},
|
|
targetAudience: {
|
|
type: DataTypes.ENUM('all', 'mobile', 'desktop'),
|
|
defaultValue: 'all',
|
|
allowNull: false
|
|
},
|
|
backgroundColor: {
|
|
type: DataTypes.STRING,
|
|
allowNull: true,
|
|
validate: {
|
|
is: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
|
|
},
|
|
comment: 'Hex color code for banner background'
|
|
},
|
|
textColor: {
|
|
type: DataTypes.STRING,
|
|
allowNull: true,
|
|
validate: {
|
|
is: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
|
|
},
|
|
comment: 'Hex color code for banner text'
|
|
},
|
|
animation: {
|
|
type: DataTypes.ENUM('none', 'fade', 'slide', 'zoom'),
|
|
defaultValue: 'none',
|
|
allowNull: false
|
|
},
|
|
metadata: {
|
|
type: DataTypes.JSONB,
|
|
allowNull: true,
|
|
defaultValue: {},
|
|
comment: 'Additional banner metadata and settings'
|
|
}
|
|
}, {
|
|
tableName: 'banners',
|
|
timestamps: true,
|
|
paranoid: true,
|
|
indexes: [
|
|
{
|
|
fields: ['isActive']
|
|
},
|
|
{
|
|
fields: ['position']
|
|
},
|
|
{
|
|
fields: ['order']
|
|
},
|
|
{
|
|
fields: ['startDate', 'endDate']
|
|
}
|
|
]
|
|
});
|
|
|
|
// Virtual field for checking if banner is currently active
|
|
Banner.prototype.isCurrentlyActive = function() {
|
|
if (!this.isActive) return false;
|
|
|
|
const now = new Date();
|
|
|
|
if (this.startDate && now < this.startDate) return false;
|
|
if (this.endDate && now > this.endDate) return false;
|
|
|
|
return true;
|
|
};
|
|
|
|
// Method to increment click count
|
|
Banner.prototype.recordClick = async function() {
|
|
this.clickCount += 1;
|
|
await this.save();
|
|
return this;
|
|
};
|
|
|
|
// Method to increment impressions
|
|
Banner.prototype.recordImpression = async function() {
|
|
this.impressions += 1;
|
|
await this.save();
|
|
return this;
|
|
};
|
|
|
|
// Static method to get active banners
|
|
Banner.getActiveBanners = async function(position = null) {
|
|
const whereClause = {
|
|
isActive: true
|
|
};
|
|
|
|
if (position) {
|
|
whereClause.position = position;
|
|
}
|
|
|
|
const now = new Date();
|
|
|
|
return await this.findAll({
|
|
where: {
|
|
...whereClause,
|
|
[require('sequelize').Op.or]: [
|
|
{ startDate: null },
|
|
{ startDate: { [require('sequelize').Op.lte]: now } }
|
|
],
|
|
[require('sequelize').Op.or]: [
|
|
{ endDate: null },
|
|
{ endDate: { [require('sequelize').Op.gte]: now } }
|
|
]
|
|
},
|
|
order: [['order', 'ASC'], ['createdAt', 'DESC']]
|
|
});
|
|
};
|
|
|
|
module.exports = Banner; |