Auto commit: 2026-04-11T08:13:09.677Z
This commit is contained in:
parent
dfebf532cf
commit
8e4bc70d03
@ -0,0 +1,189 @@
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
"SELECT to_regclass('public.marketing_campaigns') AS regclass_name;",
|
||||
{
|
||||
transaction,
|
||||
type: Sequelize.QueryTypes.SELECT,
|
||||
},
|
||||
);
|
||||
|
||||
if (rows[0].regclass_name) {
|
||||
await transaction.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
'DROP TYPE IF EXISTS "enum_marketing_campaigns_status";',
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await queryInterface.createTable(
|
||||
'marketing_campaigns',
|
||||
{
|
||||
id: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
defaultValue: Sequelize.DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
campaignName: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
campaignType: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
defaultValue: 'تسجيل الطلاب',
|
||||
},
|
||||
objective: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
audience: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
gradeFocus: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
platform: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
tone: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
offer: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
callToAction: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
publishingDate: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
notes: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
generatedContent: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
weeklyPlan: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
designerNotes: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
videoScript: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
messageTemplate: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
assetNames: {
|
||||
type: Sequelize.DataTypes.JSONB,
|
||||
allowNull: false,
|
||||
defaultValue: [],
|
||||
},
|
||||
status: {
|
||||
type: Sequelize.DataTypes.ENUM,
|
||||
values: ['draft', 'ready', 'scheduled', 'published', 'archived'],
|
||||
allowNull: false,
|
||||
defaultValue: 'draft',
|
||||
},
|
||||
isAutoGenerated: {
|
||||
type: Sequelize.DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
lastGeneratedAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
deletedAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
createdById: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
},
|
||||
updatedById: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
},
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
"SELECT to_regclass('public.marketing_campaigns') AS regclass_name;",
|
||||
{
|
||||
transaction,
|
||||
type: Sequelize.QueryTypes.SELECT,
|
||||
},
|
||||
);
|
||||
|
||||
if (!rows[0].regclass_name) {
|
||||
await transaction.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
await queryInterface.dropTable('marketing_campaigns', { transaction });
|
||||
await queryInterface.sequelize.query(
|
||||
'DROP TYPE IF EXISTS "enum_marketing_campaigns_status";',
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,145 @@
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
"SELECT to_regclass('public.marketing_schedules') AS regclass_name;",
|
||||
{
|
||||
transaction,
|
||||
type: Sequelize.QueryTypes.SELECT,
|
||||
},
|
||||
);
|
||||
|
||||
if (rows[0].regclass_name) {
|
||||
await transaction.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
'DROP TYPE IF EXISTS "enum_marketing_schedules_status";',
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await queryInterface.createTable(
|
||||
'marketing_schedules',
|
||||
{
|
||||
id: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
defaultValue: Sequelize.DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
title: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
channel: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
publishAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
type: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
status: {
|
||||
type: Sequelize.DataTypes.ENUM,
|
||||
values: ['draft', 'scheduled', 'published', 'cancelled'],
|
||||
allowNull: false,
|
||||
defaultValue: 'scheduled',
|
||||
},
|
||||
notes: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
payload: {
|
||||
type: Sequelize.DataTypes.JSONB,
|
||||
allowNull: false,
|
||||
defaultValue: {},
|
||||
},
|
||||
campaignId: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'marketing_campaigns',
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
deletedAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
createdById: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
},
|
||||
updatedById: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
},
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
"SELECT to_regclass('public.marketing_schedules') AS regclass_name;",
|
||||
{
|
||||
transaction,
|
||||
type: Sequelize.QueryTypes.SELECT,
|
||||
},
|
||||
);
|
||||
|
||||
if (!rows[0].regclass_name) {
|
||||
await transaction.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
await queryInterface.dropTable('marketing_schedules', { transaction });
|
||||
await queryInterface.sequelize.query(
|
||||
'DROP TYPE IF EXISTS "enum_marketing_schedules_status";',
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,148 @@
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
"SELECT to_regclass('public.marketing_referrals') AS regclass_name;",
|
||||
{
|
||||
transaction,
|
||||
type: Sequelize.QueryTypes.SELECT,
|
||||
},
|
||||
);
|
||||
|
||||
if (rows[0].regclass_name) {
|
||||
await transaction.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
await queryInterface.sequelize.query(
|
||||
'DROP TYPE IF EXISTS "enum_marketing_referrals_status";',
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await queryInterface.createTable(
|
||||
'marketing_referrals',
|
||||
{
|
||||
id: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
defaultValue: Sequelize.DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
referrerName: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
referrerPhone: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
studentName: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
studentGrade: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
discountAmount: {
|
||||
type: Sequelize.DataTypes.DECIMAL(10, 2),
|
||||
allowNull: true,
|
||||
},
|
||||
rewardNotes: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
status: {
|
||||
type: Sequelize.DataTypes.ENUM,
|
||||
values: ['new', 'qualified', 'rewarded', 'cancelled'],
|
||||
allowNull: false,
|
||||
defaultValue: 'new',
|
||||
},
|
||||
notes: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
inquiryId: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'student_inquiries',
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
deletedAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
createdById: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
},
|
||||
updatedById: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
},
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
"SELECT to_regclass('public.marketing_referrals') AS regclass_name;",
|
||||
{
|
||||
transaction,
|
||||
type: Sequelize.QueryTypes.SELECT,
|
||||
},
|
||||
);
|
||||
|
||||
if (!rows[0].regclass_name) {
|
||||
await transaction.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
await queryInterface.dropTable('marketing_referrals', { transaction });
|
||||
await queryInterface.sequelize.query(
|
||||
'DROP TYPE IF EXISTS "enum_marketing_referrals_status";',
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,120 @@
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
"SELECT to_regclass('public.marketing_templates') AS regclass_name;",
|
||||
{
|
||||
transaction,
|
||||
type: Sequelize.QueryTypes.SELECT,
|
||||
},
|
||||
);
|
||||
|
||||
if (rows[0].regclass_name) {
|
||||
await transaction.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
await queryInterface.createTable(
|
||||
'marketing_templates',
|
||||
{
|
||||
id: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
defaultValue: Sequelize.DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
name: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
category: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
channel: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
content: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
previewText: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
isDefault: {
|
||||
type: Sequelize.DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
deletedAt: {
|
||||
type: Sequelize.DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
createdById: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
},
|
||||
updatedById: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
allowNull: true,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE',
|
||||
},
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const rows = await queryInterface.sequelize.query(
|
||||
"SELECT to_regclass('public.marketing_templates') AS regclass_name;",
|
||||
{
|
||||
transaction,
|
||||
type: Sequelize.QueryTypes.SELECT,
|
||||
},
|
||||
);
|
||||
|
||||
if (!rows[0].regclass_name) {
|
||||
await transaction.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
await queryInterface.dropTable('marketing_templates', { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
119
backend/src/db/models/marketing_campaigns.js
Normal file
119
backend/src/db/models/marketing_campaigns.js
Normal file
@ -0,0 +1,119 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const marketing_campaigns = sequelize.define(
|
||||
'marketing_campaigns',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
campaignName: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
campaignType: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
defaultValue: 'تسجيل الطلاب',
|
||||
},
|
||||
objective: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
audience: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
gradeFocus: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
platform: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
tone: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
offer: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
callToAction: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
publishingDate: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
generatedContent: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
weeklyPlan: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
designerNotes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
videoScript: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
messageTemplate: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
assetNames: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false,
|
||||
defaultValue: [],
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM,
|
||||
values: ['draft', 'ready', 'scheduled', 'published', 'archived'],
|
||||
allowNull: false,
|
||||
defaultValue: 'draft',
|
||||
},
|
||||
isAutoGenerated: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
lastGeneratedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
paranoid: true,
|
||||
freezeTableName: true,
|
||||
},
|
||||
);
|
||||
|
||||
marketing_campaigns.associate = (db) => {
|
||||
db.marketing_campaigns.hasMany(db.marketing_schedules, {
|
||||
as: 'schedules',
|
||||
foreignKey: 'campaignId',
|
||||
});
|
||||
|
||||
db.marketing_campaigns.belongsTo(db.users, {
|
||||
as: 'createdBy',
|
||||
});
|
||||
|
||||
db.marketing_campaigns.belongsTo(db.users, {
|
||||
as: 'updatedBy',
|
||||
});
|
||||
};
|
||||
|
||||
return marketing_campaigns;
|
||||
};
|
||||
68
backend/src/db/models/marketing_referrals.js
Normal file
68
backend/src/db/models/marketing_referrals.js
Normal file
@ -0,0 +1,68 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const marketing_referrals = sequelize.define(
|
||||
'marketing_referrals',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
referrerName: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
referrerPhone: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
studentName: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
studentGrade: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
discountAmount: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: true,
|
||||
},
|
||||
rewardNotes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM,
|
||||
values: ['new', 'qualified', 'rewarded', 'cancelled'],
|
||||
allowNull: false,
|
||||
defaultValue: 'new',
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
paranoid: true,
|
||||
freezeTableName: true,
|
||||
},
|
||||
);
|
||||
|
||||
marketing_referrals.associate = (db) => {
|
||||
db.marketing_referrals.belongsTo(db.student_inquiries, {
|
||||
as: 'inquiry',
|
||||
foreignKey: 'inquiryId',
|
||||
});
|
||||
|
||||
db.marketing_referrals.belongsTo(db.users, {
|
||||
as: 'createdBy',
|
||||
});
|
||||
|
||||
db.marketing_referrals.belongsTo(db.users, {
|
||||
as: 'updatedBy',
|
||||
});
|
||||
};
|
||||
|
||||
return marketing_referrals;
|
||||
};
|
||||
65
backend/src/db/models/marketing_schedules.js
Normal file
65
backend/src/db/models/marketing_schedules.js
Normal file
@ -0,0 +1,65 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const marketing_schedules = sequelize.define(
|
||||
'marketing_schedules',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
channel: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
publishAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM,
|
||||
values: ['draft', 'scheduled', 'published', 'cancelled'],
|
||||
allowNull: false,
|
||||
defaultValue: 'scheduled',
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
payload: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false,
|
||||
defaultValue: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
paranoid: true,
|
||||
freezeTableName: true,
|
||||
},
|
||||
);
|
||||
|
||||
marketing_schedules.associate = (db) => {
|
||||
db.marketing_schedules.belongsTo(db.marketing_campaigns, {
|
||||
as: 'campaign',
|
||||
foreignKey: 'campaignId',
|
||||
});
|
||||
|
||||
db.marketing_schedules.belongsTo(db.users, {
|
||||
as: 'createdBy',
|
||||
});
|
||||
|
||||
db.marketing_schedules.belongsTo(db.users, {
|
||||
as: 'updatedBy',
|
||||
});
|
||||
};
|
||||
|
||||
return marketing_schedules;
|
||||
};
|
||||
54
backend/src/db/models/marketing_templates.js
Normal file
54
backend/src/db/models/marketing_templates.js
Normal file
@ -0,0 +1,54 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const marketing_templates = sequelize.define(
|
||||
'marketing_templates',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
category: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
channel: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
previewText: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
isDefault: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
paranoid: true,
|
||||
freezeTableName: true,
|
||||
},
|
||||
);
|
||||
|
||||
marketing_templates.associate = (db) => {
|
||||
db.marketing_templates.belongsTo(db.users, {
|
||||
as: 'createdBy',
|
||||
});
|
||||
|
||||
db.marketing_templates.belongsTo(db.users, {
|
||||
as: 'updatedBy',
|
||||
});
|
||||
};
|
||||
|
||||
return marketing_templates;
|
||||
};
|
||||
@ -38,6 +38,7 @@ const app_settingsRoutes = require('./routes/app_settings');
|
||||
|
||||
const api_keysRoutes = require('./routes/api_keys');
|
||||
const studentInquiriesRoutes = require('./routes/student_inquiries');
|
||||
const schoolMarketingRoutes = require('./routes/school_marketing');
|
||||
|
||||
|
||||
const getBaseUrl = (url) => {
|
||||
@ -115,6 +116,7 @@ app.use('/api/app_settings', passport.authenticate('jwt', {session: false}), app
|
||||
|
||||
app.use('/api/api_keys', passport.authenticate('jwt', {session: false}), api_keysRoutes);
|
||||
app.use('/api/student-inquiries', studentInquiriesRoutes);
|
||||
app.use('/api/school-marketing', schoolMarketingRoutes);
|
||||
|
||||
app.use(
|
||||
'/api/openai',
|
||||
|
||||
609
backend/src/routes/school_marketing.js
Normal file
609
backend/src/routes/school_marketing.js
Normal file
@ -0,0 +1,609 @@
|
||||
const express = require('express');
|
||||
const passport = require('passport');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
const db = require('../db/models');
|
||||
const Helpers = require('../helpers');
|
||||
|
||||
const router = express.Router();
|
||||
const wrapAsync = Helpers.wrapAsync;
|
||||
|
||||
const DEFAULT_TEMPLATES = [
|
||||
{
|
||||
name: 'ترحيب واتساب لأولياء الأمور',
|
||||
category: 'واتساب',
|
||||
channel: 'واتساب',
|
||||
content: 'مرحبًا بكم في {{schoolName}}. يسعدنا استقبالكم وتعريفكم ببرامجنا التعليمية. هل يناسبكم موعد زيارة هذا الأسبوع؟',
|
||||
previewText: 'رسالة أول تواصل سريعة ومحترفة.',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: 'إعلان التسجيل المبكر',
|
||||
category: 'إعلان',
|
||||
channel: 'إنستغرام',
|
||||
content: 'ابدأ مستقبل طفلك مع {{schoolName}}. التسجيل المبكر متاح الآن مع مزايا خاصة ومقاعد محدودة.',
|
||||
previewText: 'نص إعلان قصير للحملات المدفوعة.',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: 'سكربت فيديو تعريفي',
|
||||
category: 'فيديو',
|
||||
channel: 'فيديو',
|
||||
content: 'المشهد 1: لقطات للمدرسة. المشهد 2: الأنشطة. المشهد 3: رسالة ثقة للأهالي. المشهد 4: دعوة لحجز زيارة.',
|
||||
previewText: 'هيكل أولي لفيديو قصير.',
|
||||
isDefault: true,
|
||||
},
|
||||
];
|
||||
|
||||
const CAMPAIGN_STATUSES = ['draft', 'ready', 'scheduled', 'published', 'archived'];
|
||||
const SCHEDULE_STATUSES = ['draft', 'scheduled', 'published', 'cancelled'];
|
||||
const REFERRAL_STATUSES = ['new', 'qualified', 'rewarded', 'cancelled'];
|
||||
|
||||
function makeBadRequest(message) {
|
||||
const error = new Error(message);
|
||||
error.code = 400;
|
||||
return error;
|
||||
}
|
||||
|
||||
function makeNotFound(message) {
|
||||
const error = new Error(message);
|
||||
error.code = 404;
|
||||
return error;
|
||||
}
|
||||
|
||||
function cleanText(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function normalizeNullableText(value) {
|
||||
const cleaned = cleanText(value);
|
||||
return cleaned || null;
|
||||
}
|
||||
|
||||
function normalizeDate(value, fieldName, required = false) {
|
||||
const cleaned = cleanText(value);
|
||||
|
||||
if (!cleaned) {
|
||||
if (required) {
|
||||
throw makeBadRequest(`${fieldName} is required`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = new Date(cleaned);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
throw makeBadRequest(`${fieldName} is invalid`);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function normalizeAssetNames(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return value
|
||||
.map((item) => cleanText(item))
|
||||
.filter(Boolean)
|
||||
.slice(0, 50);
|
||||
}
|
||||
|
||||
function buildCampaignPayload(data = {}) {
|
||||
const campaignName = cleanText(data.campaignName);
|
||||
if (!campaignName) {
|
||||
throw makeBadRequest('Campaign name is required');
|
||||
}
|
||||
|
||||
const status = cleanText(data.status) || 'draft';
|
||||
if (!CAMPAIGN_STATUSES.includes(status)) {
|
||||
throw makeBadRequest('Campaign status is invalid');
|
||||
}
|
||||
|
||||
return {
|
||||
campaignName,
|
||||
campaignType: cleanText(data.campaignType) || 'تسجيل الطلاب',
|
||||
objective: normalizeNullableText(data.objective),
|
||||
audience: normalizeNullableText(data.audience),
|
||||
gradeFocus: normalizeNullableText(data.gradeFocus),
|
||||
platform: normalizeNullableText(data.platform),
|
||||
tone: normalizeNullableText(data.tone),
|
||||
offer: normalizeNullableText(data.offer),
|
||||
callToAction: normalizeNullableText(data.callToAction),
|
||||
publishingDate: normalizeDate(data.publishingDate, 'Publishing date'),
|
||||
notes: normalizeNullableText(data.notes),
|
||||
generatedContent: normalizeNullableText(data.generatedContent),
|
||||
weeklyPlan: normalizeNullableText(data.weeklyPlan),
|
||||
designerNotes: normalizeNullableText(data.designerNotes),
|
||||
videoScript: normalizeNullableText(data.videoScript),
|
||||
messageTemplate: normalizeNullableText(data.messageTemplate),
|
||||
assetNames: normalizeAssetNames(data.assetNames),
|
||||
status,
|
||||
isAutoGenerated: Boolean(data.isAutoGenerated),
|
||||
lastGeneratedAt: data.generatedContent || data.weeklyPlan || data.videoScript ? new Date() : null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSchedulePayload(data = {}) {
|
||||
const title = cleanText(data.title);
|
||||
if (!title) {
|
||||
throw makeBadRequest('Schedule title is required');
|
||||
}
|
||||
|
||||
const status = cleanText(data.status) || 'scheduled';
|
||||
if (!SCHEDULE_STATUSES.includes(status)) {
|
||||
throw makeBadRequest('Schedule status is invalid');
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
channel: normalizeNullableText(data.channel),
|
||||
publishAt: normalizeDate(data.publishAt, 'Publish date', true),
|
||||
type: normalizeNullableText(data.type),
|
||||
status,
|
||||
notes: normalizeNullableText(data.notes),
|
||||
payload: typeof data.payload === 'object' && data.payload !== null ? data.payload : {},
|
||||
campaignId: cleanText(data.campaignId) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildTemplatePayload(data = {}) {
|
||||
const name = cleanText(data.name);
|
||||
const content = cleanText(data.content);
|
||||
|
||||
if (!name) {
|
||||
throw makeBadRequest('Template name is required');
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
throw makeBadRequest('Template content is required');
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
category: normalizeNullableText(data.category),
|
||||
channel: normalizeNullableText(data.channel),
|
||||
content,
|
||||
previewText: normalizeNullableText(data.previewText),
|
||||
isDefault: Boolean(data.isDefault),
|
||||
};
|
||||
}
|
||||
|
||||
function buildReferralPayload(data = {}) {
|
||||
const referrerName = cleanText(data.referrerName);
|
||||
const studentName = cleanText(data.studentName);
|
||||
const status = cleanText(data.status) || 'new';
|
||||
|
||||
if (!referrerName) {
|
||||
throw makeBadRequest('Referrer name is required');
|
||||
}
|
||||
|
||||
if (!studentName) {
|
||||
throw makeBadRequest('Student name is required');
|
||||
}
|
||||
|
||||
if (!REFERRAL_STATUSES.includes(status)) {
|
||||
throw makeBadRequest('Referral status is invalid');
|
||||
}
|
||||
|
||||
return {
|
||||
referrerName,
|
||||
referrerPhone: normalizeNullableText(data.referrerPhone),
|
||||
studentName,
|
||||
studentGrade: normalizeNullableText(data.studentGrade),
|
||||
discountAmount: data.discountAmount !== undefined && data.discountAmount !== null && `${data.discountAmount}` !== ''
|
||||
? Number(data.discountAmount)
|
||||
: null,
|
||||
rewardNotes: normalizeNullableText(data.rewardNotes),
|
||||
status,
|
||||
notes: normalizeNullableText(data.notes),
|
||||
inquiryId: cleanText(data.inquiryId) || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureDefaultTemplates(currentUserId) {
|
||||
const count = await db.marketing_templates.count();
|
||||
if (count > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
await db.marketing_templates.bulkCreate(
|
||||
DEFAULT_TEMPLATES.map((item, index) => ({
|
||||
...item,
|
||||
createdById: currentUserId || null,
|
||||
updatedById: currentUserId || null,
|
||||
createdAt: new Date(now.getTime() + index * 1000),
|
||||
updatedAt: new Date(now.getTime() + index * 1000),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
router.use(passport.authenticate('jwt', { session: false }));
|
||||
|
||||
router.get(
|
||||
'/workspace',
|
||||
wrapAsync(async (req, res) => {
|
||||
await ensureDefaultTemplates(req.currentUser?.id || null);
|
||||
|
||||
const [
|
||||
summary,
|
||||
inquiries,
|
||||
campaigns,
|
||||
schedules,
|
||||
referrals,
|
||||
templates,
|
||||
latestDraft,
|
||||
] = await Promise.all([
|
||||
db.student_inquiries.count().then(async (total) => ({
|
||||
total,
|
||||
newLeads: await db.student_inquiries.count({ where: { status: 'new' } }),
|
||||
scheduled: await db.student_inquiries.count({ where: { status: 'demo_scheduled' } }),
|
||||
enrolled: await db.student_inquiries.count({ where: { status: 'enrolled' } }),
|
||||
referrals: await db.marketing_referrals.count(),
|
||||
campaigns: await db.marketing_campaigns.count(),
|
||||
scheduledPosts: await db.marketing_schedules.count({ where: { status: 'scheduled' } }),
|
||||
})),
|
||||
db.student_inquiries.findAll({ order: [['createdAt', 'DESC']], limit: 100 }),
|
||||
db.marketing_campaigns.findAll({ order: [['updatedAt', 'DESC']], limit: 20 }),
|
||||
db.marketing_schedules.findAll({ order: [['publishAt', 'ASC']], limit: 20 }),
|
||||
db.marketing_referrals.findAll({ order: [['createdAt', 'DESC']], limit: 20 }),
|
||||
db.marketing_templates.findAll({ order: [['isDefault', 'DESC'], ['updatedAt', 'DESC']], limit: 20 }),
|
||||
db.marketing_campaigns.findOne({
|
||||
where: { status: 'draft' },
|
||||
order: [['updatedAt', 'DESC']],
|
||||
}),
|
||||
]);
|
||||
|
||||
res.status(200).send({
|
||||
summary,
|
||||
inquiries,
|
||||
campaigns,
|
||||
schedules,
|
||||
referrals,
|
||||
templates,
|
||||
latestDraft,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/campaigns',
|
||||
wrapAsync(async (req, res) => {
|
||||
const status = cleanText(req.query.status);
|
||||
const q = cleanText(req.query.q);
|
||||
const where = {};
|
||||
|
||||
if (status) {
|
||||
if (!CAMPAIGN_STATUSES.includes(status)) {
|
||||
throw makeBadRequest('Campaign status filter is invalid');
|
||||
}
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
if (q) {
|
||||
where[Op.or] = [
|
||||
{ campaignName: { [Op.iLike]: `%${q}%` } },
|
||||
{ campaignType: { [Op.iLike]: `%${q}%` } },
|
||||
{ platform: { [Op.iLike]: `%${q}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
const rows = await db.marketing_campaigns.findAll({
|
||||
where,
|
||||
order: [['updatedAt', 'DESC']],
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
res.status(200).send({ rows, count: rows.length });
|
||||
}),
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/campaigns',
|
||||
wrapAsync(async (req, res) => {
|
||||
const payload = buildCampaignPayload(req.body.data || {});
|
||||
const campaign = await db.marketing_campaigns.create({
|
||||
...payload,
|
||||
createdById: req.currentUser?.id || null,
|
||||
updatedById: req.currentUser?.id || null,
|
||||
});
|
||||
|
||||
res.status(200).send(campaign);
|
||||
}),
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/campaigns/save-draft',
|
||||
wrapAsync(async (req, res) => {
|
||||
const payload = buildCampaignPayload({ ...(req.body.data || {}), status: 'draft' });
|
||||
const campaignId = cleanText(req.body.data?.id);
|
||||
|
||||
let campaign;
|
||||
if (campaignId) {
|
||||
campaign = await db.marketing_campaigns.findByPk(campaignId);
|
||||
if (!campaign) {
|
||||
throw makeNotFound('Campaign not found');
|
||||
}
|
||||
|
||||
await campaign.update({
|
||||
...payload,
|
||||
updatedById: req.currentUser?.id || null,
|
||||
});
|
||||
} else {
|
||||
campaign = await db.marketing_campaigns.create({
|
||||
...payload,
|
||||
createdById: req.currentUser?.id || null,
|
||||
updatedById: req.currentUser?.id || null,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).send(campaign);
|
||||
}),
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/campaigns/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
const campaign = await db.marketing_campaigns.findByPk(req.params.id);
|
||||
if (!campaign) {
|
||||
throw makeNotFound('Campaign not found');
|
||||
}
|
||||
|
||||
const payload = buildCampaignPayload(req.body.data || {});
|
||||
await campaign.update({
|
||||
...payload,
|
||||
updatedById: req.currentUser?.id || null,
|
||||
});
|
||||
|
||||
res.status(200).send(campaign);
|
||||
}),
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/campaigns/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
const campaign = await db.marketing_campaigns.findByPk(req.params.id);
|
||||
if (!campaign) {
|
||||
throw makeNotFound('Campaign not found');
|
||||
}
|
||||
|
||||
const relatedSchedules = await db.marketing_schedules.findAll({
|
||||
where: { campaignId: campaign.id },
|
||||
});
|
||||
|
||||
await Promise.all(relatedSchedules.map(async (schedule) => {
|
||||
await schedule.update({ updatedById: req.currentUser?.id || null });
|
||||
await schedule.destroy();
|
||||
}));
|
||||
|
||||
await campaign.update({ updatedById: req.currentUser?.id || null });
|
||||
await campaign.destroy();
|
||||
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/schedules',
|
||||
wrapAsync(async (req, res) => {
|
||||
const rows = await db.marketing_schedules.findAll({
|
||||
order: [['publishAt', 'ASC']],
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
res.status(200).send({ rows, count: rows.length });
|
||||
}),
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/schedules',
|
||||
wrapAsync(async (req, res) => {
|
||||
const payload = buildSchedulePayload(req.body.data || {});
|
||||
const schedule = await db.marketing_schedules.create({
|
||||
...payload,
|
||||
createdById: req.currentUser?.id || null,
|
||||
updatedById: req.currentUser?.id || null,
|
||||
});
|
||||
|
||||
if (payload.campaignId) {
|
||||
const campaign = await db.marketing_campaigns.findByPk(payload.campaignId);
|
||||
if (campaign) {
|
||||
await campaign.update({
|
||||
status: 'scheduled',
|
||||
publishingDate: payload.publishAt,
|
||||
updatedById: req.currentUser?.id || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).send(schedule);
|
||||
}),
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/schedules/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
const schedule = await db.marketing_schedules.findByPk(req.params.id);
|
||||
if (!schedule) {
|
||||
throw makeNotFound('Schedule not found');
|
||||
}
|
||||
|
||||
const payload = buildSchedulePayload(req.body.data || {});
|
||||
await schedule.update({
|
||||
...payload,
|
||||
updatedById: req.currentUser?.id || null,
|
||||
});
|
||||
|
||||
if (payload.campaignId) {
|
||||
const campaign = await db.marketing_campaigns.findByPk(payload.campaignId);
|
||||
if (campaign) {
|
||||
let nextStatus = 'scheduled';
|
||||
|
||||
if (payload.status === 'published') {
|
||||
nextStatus = 'published';
|
||||
}
|
||||
|
||||
if (payload.status === 'cancelled') {
|
||||
nextStatus = 'ready';
|
||||
}
|
||||
|
||||
await campaign.update({
|
||||
status: nextStatus,
|
||||
publishingDate: payload.publishAt,
|
||||
updatedById: req.currentUser?.id || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).send(schedule);
|
||||
}),
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/schedules/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
const schedule = await db.marketing_schedules.findByPk(req.params.id);
|
||||
if (!schedule) {
|
||||
throw makeNotFound('Schedule not found');
|
||||
}
|
||||
|
||||
await schedule.update({ updatedById: req.currentUser?.id || null });
|
||||
await schedule.destroy();
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/templates',
|
||||
wrapAsync(async (req, res) => {
|
||||
await ensureDefaultTemplates(req.currentUser?.id || null);
|
||||
const rows = await db.marketing_templates.findAll({
|
||||
order: [['isDefault', 'DESC'], ['updatedAt', 'DESC']],
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
res.status(200).send({ rows, count: rows.length });
|
||||
}),
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/templates',
|
||||
wrapAsync(async (req, res) => {
|
||||
const payload = buildTemplatePayload(req.body.data || {});
|
||||
const template = await db.marketing_templates.create({
|
||||
...payload,
|
||||
createdById: req.currentUser?.id || null,
|
||||
updatedById: req.currentUser?.id || null,
|
||||
});
|
||||
|
||||
res.status(200).send(template);
|
||||
}),
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/templates/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
const template = await db.marketing_templates.findByPk(req.params.id);
|
||||
if (!template) {
|
||||
throw makeNotFound('Template not found');
|
||||
}
|
||||
|
||||
const payload = buildTemplatePayload(req.body.data || {});
|
||||
await template.update({
|
||||
...payload,
|
||||
updatedById: req.currentUser?.id || null,
|
||||
});
|
||||
|
||||
res.status(200).send(template);
|
||||
}),
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/templates/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
const template = await db.marketing_templates.findByPk(req.params.id);
|
||||
if (!template) {
|
||||
throw makeNotFound('Template not found');
|
||||
}
|
||||
|
||||
await template.update({ updatedById: req.currentUser?.id || null });
|
||||
await template.destroy();
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/referrals',
|
||||
wrapAsync(async (req, res) => {
|
||||
const rows = await db.marketing_referrals.findAll({
|
||||
include: [
|
||||
{
|
||||
model: db.student_inquiries,
|
||||
as: 'inquiry',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
res.status(200).send({ rows, count: rows.length });
|
||||
}),
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/referrals',
|
||||
wrapAsync(async (req, res) => {
|
||||
const payload = buildReferralPayload(req.body.data || {});
|
||||
const referral = await db.marketing_referrals.create({
|
||||
...payload,
|
||||
createdById: req.currentUser?.id || null,
|
||||
updatedById: req.currentUser?.id || null,
|
||||
});
|
||||
|
||||
res.status(200).send(referral);
|
||||
}),
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/referrals/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
const referral = await db.marketing_referrals.findByPk(req.params.id);
|
||||
if (!referral) {
|
||||
throw makeNotFound('Referral not found');
|
||||
}
|
||||
|
||||
const payload = buildReferralPayload(req.body.data || {});
|
||||
await referral.update({
|
||||
...payload,
|
||||
updatedById: req.currentUser?.id || null,
|
||||
});
|
||||
|
||||
res.status(200).send(referral);
|
||||
}),
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/referrals/:id',
|
||||
wrapAsync(async (req, res) => {
|
||||
const referral = await db.marketing_referrals.findByPk(req.params.id);
|
||||
if (!referral) {
|
||||
throw makeNotFound('Referral not found');
|
||||
}
|
||||
|
||||
await referral.update({ updatedById: req.currentUser?.id || null });
|
||||
await referral.destroy();
|
||||
|
||||
res.status(200).send(true);
|
||||
}),
|
||||
);
|
||||
|
||||
router.use('/', Helpers.commonErrorHandler);
|
||||
|
||||
module.exports = router;
|
||||
@ -5,12 +5,12 @@ const menuAside: MenuAsideItem[] = [
|
||||
{
|
||||
href: '/dashboard',
|
||||
icon: icon.mdiViewDashboardOutline,
|
||||
label: 'Dashboard',
|
||||
label: 'لوحة التحكم',
|
||||
},
|
||||
|
||||
{
|
||||
href: '/school-marketing',
|
||||
label: 'التسويق المدرسي',
|
||||
label: 'استوديو التسويق المدرسي',
|
||||
icon: icon.mdiBullhornOutline,
|
||||
},
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user