123
This commit is contained in:
parent
b7815f3517
commit
de3ba95d58
848
backend/scripts/seed-furniture-demo-data.js
Normal file
848
backend/scripts/seed-furniture-demo-data.js
Normal file
@ -0,0 +1,848 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const config = require('../src/config');
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.DEMO_DATA_API_URL || 'http://127.0.0.1:3000/api';
|
||||||
|
const LOGIN_EMAIL = process.env.DEMO_DATA_EMAIL || config.admin_email;
|
||||||
|
const LOGIN_PASSWORD = process.env.DEMO_DATA_PASSWORD || config.admin_pass;
|
||||||
|
const LIST_LIMIT = 200;
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const summary = {
|
||||||
|
social_channels: { created: 0, updated: 0 },
|
||||||
|
products: { created: 0, updated: 0 },
|
||||||
|
case_studies: { created: 0, updated: 0 },
|
||||||
|
content_briefs: { created: 0, updated: 0 },
|
||||||
|
content_ideas: { created: 0, updated: 0 },
|
||||||
|
content_plan_items: { created: 0, updated: 0 },
|
||||||
|
generated_contents: { created: 0, updated: 0 },
|
||||||
|
leads: { created: 0, updated: 0 },
|
||||||
|
lead_activities: { created: 0, updated: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response) {
|
||||||
|
console.error('API request failed:', {
|
||||||
|
method: error.config?.method,
|
||||||
|
url: error.config?.url,
|
||||||
|
status: error.response.status,
|
||||||
|
response: error.response.data,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Request failed before response:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function track(resource, action) {
|
||||||
|
summary[resource][action] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeImportHash(parts) {
|
||||||
|
return ['furniture-demo', ...parts].join(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateOffset(daysOffset, hour, minute = 0) {
|
||||||
|
const value = new Date();
|
||||||
|
value.setUTCDate(value.getUTCDate() + daysOffset);
|
||||||
|
value.setUTCHours(hour, minute, 0, 0);
|
||||||
|
return value.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
const { data: token } = await api.post('/auth/signin/local', {
|
||||||
|
email: LOGIN_EMAIL,
|
||||||
|
password: LOGIN_PASSWORD,
|
||||||
|
});
|
||||||
|
|
||||||
|
api.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||||
|
|
||||||
|
const { data: currentUser } = await api.get('/auth/me');
|
||||||
|
|
||||||
|
return currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function list(resource) {
|
||||||
|
const rows = [];
|
||||||
|
let page = 0;
|
||||||
|
|
||||||
|
while (page < 20) {
|
||||||
|
const { data } = await api.get(`/${resource}?limit=${LIST_LIMIT}&page=${page}`);
|
||||||
|
const batch = Array.isArray(data.rows) ? data.rows : [];
|
||||||
|
rows.push(...batch);
|
||||||
|
|
||||||
|
if (batch.length < LIST_LIMIT) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findExisting(rows, payload, matchFields = []) {
|
||||||
|
if (payload.importHash) {
|
||||||
|
const matchByImportHash = rows.find((item) => item.importHash === payload.importHash);
|
||||||
|
|
||||||
|
if (matchByImportHash) {
|
||||||
|
return matchByImportHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchFields.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.find((item) =>
|
||||||
|
matchFields.every((field) => normalize(item[field]) === normalize(payload[field])),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findByImportHashInDatabase(resource, importHash) {
|
||||||
|
if (!importHash) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[a-z_]+$/.test(resource)) {
|
||||||
|
throw new Error(`Unsafe resource name: ${resource}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapedImportHash = importHash.replace(/'/g, );
|
||||||
|
const sql = `SELECT id, "importHash" FROM ${resource} WHERE "importHash" = '${escapedImportHash}' LIMIT 1`;
|
||||||
|
const { data } = await api.post('/sql', { sql });
|
||||||
|
|
||||||
|
return Array.isArray(data.rows) && data.rows.length ? data.rows[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsert(resource, rows, payload, options = {}) {
|
||||||
|
const { matchFields = [], labelFields = [] } = options;
|
||||||
|
let existing = findExisting(rows, payload, matchFields);
|
||||||
|
const label = labelFields.map((field) => payload[field]).find(Boolean) || payload.importHash || resource;
|
||||||
|
|
||||||
|
if (!existing && payload.importHash) {
|
||||||
|
existing = await findByImportHashInDatabase(resource, payload.importHash);
|
||||||
|
|
||||||
|
if (existing && !rows.some((item) => item.id === existing.id)) {
|
||||||
|
rows.push(existing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await api.put(`/${resource}/${existing.id}`, {
|
||||||
|
id: existing.id,
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
track(resource, 'updated');
|
||||||
|
console.log(`↺ Updated ${resource}: ${label}`);
|
||||||
|
|
||||||
|
const updatedRow = {
|
||||||
|
...existing,
|
||||||
|
...payload,
|
||||||
|
id: existing.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const index = rows.findIndex((item) => item.id === existing.id);
|
||||||
|
if (index >= 0) {
|
||||||
|
rows[index] = updatedRow;
|
||||||
|
} else {
|
||||||
|
rows.push(updatedRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: createResponse } = await api.post(`/${resource}`, {
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
let created = createResponse;
|
||||||
|
|
||||||
|
if (!created || !created.id) {
|
||||||
|
created = (payload.importHash && (await findByImportHashInDatabase(resource, payload.importHash))) || null;
|
||||||
|
|
||||||
|
if (!created) {
|
||||||
|
const refreshedRows = await list(resource);
|
||||||
|
created = findExisting(refreshedRows, payload, matchFields);
|
||||||
|
rows.splice(0, rows.length, ...refreshedRows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!created || !created.id) {
|
||||||
|
throw new Error(`Created ${resource} item could not be located afterwards: ${label}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
track(resource, 'created');
|
||||||
|
|
||||||
|
if (!rows.some((item) => item.id === created.id)) {
|
||||||
|
rows.push(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`+ Created ${resource}: ${label}`);
|
||||||
|
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log(`Connecting to ${API_BASE_URL}`);
|
||||||
|
let currentUser = await login();
|
||||||
|
|
||||||
|
const organizationName = currentUser.organization?.name || 'Фабрика мебели на заказ';
|
||||||
|
const organizationId = currentUser.organization?.id || null;
|
||||||
|
const ownerUserId = currentUser.id;
|
||||||
|
|
||||||
|
console.log(`Signed in as ${currentUser.email} (${organizationName})`);
|
||||||
|
|
||||||
|
const [
|
||||||
|
socialChannels,
|
||||||
|
products,
|
||||||
|
caseStudies,
|
||||||
|
contentBriefs,
|
||||||
|
contentIdeas,
|
||||||
|
contentPlanItems,
|
||||||
|
generatedContents,
|
||||||
|
leads,
|
||||||
|
leadActivities,
|
||||||
|
] = await Promise.all([
|
||||||
|
list('social_channels'),
|
||||||
|
list('products'),
|
||||||
|
list('case_studies'),
|
||||||
|
list('content_briefs'),
|
||||||
|
list('content_ideas'),
|
||||||
|
list('content_plan_items'),
|
||||||
|
list('generated_contents'),
|
||||||
|
list('leads'),
|
||||||
|
list('lead_activities'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const channels = {};
|
||||||
|
for (const channelPayload of [
|
||||||
|
{
|
||||||
|
key: 'instagram_showroom',
|
||||||
|
platform: 'instagram',
|
||||||
|
account_name: 'atelier_forma_msk',
|
||||||
|
account_url: 'https://instagram.com/atelier_forma_msk',
|
||||||
|
access_status: 'connected',
|
||||||
|
notes: `Основная витрина ${organizationName}: кейсы, до/после, рилсы из цеха.`,
|
||||||
|
is_active: true,
|
||||||
|
importHash: makeImportHash(['social_channels', 'instagram-showroom']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'telegram_channel',
|
||||||
|
platform: 'telegram',
|
||||||
|
account_name: '@atelier_forma_design',
|
||||||
|
account_url: 'https://t.me/atelier_forma_design',
|
||||||
|
access_status: 'connected',
|
||||||
|
notes: 'Канал с кейсами, материалами, ответами на частые вопросы и CTA на замер.',
|
||||||
|
is_active: true,
|
||||||
|
importHash: makeImportHash(['social_channels', 'telegram-channel']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vk_local',
|
||||||
|
platform: 'vk',
|
||||||
|
account_name: 'atelier_forma_custom',
|
||||||
|
account_url: 'https://vk.com/atelier_forma_custom',
|
||||||
|
access_status: 'connected',
|
||||||
|
notes: 'Локальное продвижение, отзывы клиентов и подборки реализованных проектов.',
|
||||||
|
is_active: true,
|
||||||
|
importHash: makeImportHash(['social_channels', 'vk-local']),
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
const savedChannel = await upsert('social_channels', socialChannels, channelPayload, {
|
||||||
|
matchFields: ['account_name'],
|
||||||
|
labelFields: ['account_name'],
|
||||||
|
});
|
||||||
|
|
||||||
|
channels[channelPayload.key] = savedChannel.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const productIds = {};
|
||||||
|
for (const productPayload of [
|
||||||
|
{
|
||||||
|
key: 'premium_kitchens',
|
||||||
|
category: 'kitchen',
|
||||||
|
name: 'Премиальные кухни на заказ',
|
||||||
|
description:
|
||||||
|
'Кухни под размеры клиента: остров, пеналы до потолка, встроенная техника и продуманная эргономика.',
|
||||||
|
starting_price: 390000,
|
||||||
|
materials: 'МДФ эмаль, дубовый шпон, HPL-пластик, кварцевые столешницы.',
|
||||||
|
features: 'Blum, скрытая подсветка, внутренние органайзеры, проект и монтаж под ключ.',
|
||||||
|
is_active: true,
|
||||||
|
importHash: makeImportHash(['products', 'premium-kitchens']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'wardrobes_to_ceiling',
|
||||||
|
category: 'wardrobe',
|
||||||
|
name: 'Шкафы-купе до потолка',
|
||||||
|
description:
|
||||||
|
'Шкафы для прихожих, спален и ниш с точной подгонкой под геометрию помещения.',
|
||||||
|
starting_price: 210000,
|
||||||
|
materials: 'ЛДСП Egger, МДФ в эмали, алюминиевые профили, стекло и зеркало.',
|
||||||
|
features: 'Максимум хранения, антресоли, подсветка полок, скрытые ручки.',
|
||||||
|
is_active: true,
|
||||||
|
importHash: makeImportHash(['products', 'wardrobes-to-ceiling']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'walk_in_closets',
|
||||||
|
category: 'closet',
|
||||||
|
name: 'Гардеробные системы под размер',
|
||||||
|
description:
|
||||||
|
'Гардеробные для мастер-спален и отдельных комнат: открытые секции, островные тумбы и зеркала.',
|
||||||
|
starting_price: 340000,
|
||||||
|
materials: 'Шпон, ЛДСП премиум-класса, металл, тонированное стекло.',
|
||||||
|
features: 'Сценарии хранения, секции под обувь и сумки, мягкая подсветка, доводчики.',
|
||||||
|
is_active: true,
|
||||||
|
importHash: makeImportHash(['products', 'walk-in-closets']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'living_room_systems',
|
||||||
|
category: 'living_room',
|
||||||
|
name: 'ТВ-зоны и системы хранения для гостиной',
|
||||||
|
description:
|
||||||
|
'Композиции для гостиной с витринами, скрытыми модулями и зонами под технику.',
|
||||||
|
starting_price: 280000,
|
||||||
|
materials: 'Шпон ореха, матовые фасады, фрезерованные панели, металл.',
|
||||||
|
features: 'Скрытая проводка, витрины с подсветкой, комбинация открытого и закрытого хранения.',
|
||||||
|
is_active: true,
|
||||||
|
importHash: makeImportHash(['products', 'living-room-systems']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'home_offices',
|
||||||
|
category: 'office',
|
||||||
|
name: 'Домашние кабинеты и библиотеки',
|
||||||
|
description:
|
||||||
|
'Рабочие зоны, библиотеки и мебель для кабинета с эргономикой под ежедневную нагрузку.',
|
||||||
|
starting_price: 260000,
|
||||||
|
materials: 'Шпон дуба, эмаль, акустические панели, металл в порошковой окраске.',
|
||||||
|
features: 'Кабель-менеджмент, столешницы увеличенной глубины, закрытые модули, подсветка.',
|
||||||
|
is_active: true,
|
||||||
|
importHash: makeImportHash(['products', 'home-offices']),
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
const savedProduct = await upsert('products', products, productPayload, {
|
||||||
|
matchFields: ['name'],
|
||||||
|
labelFields: ['name'],
|
||||||
|
});
|
||||||
|
|
||||||
|
productIds[productPayload.key] = savedProduct.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const caseStudyIds = {};
|
||||||
|
for (const caseStudyPayload of [
|
||||||
|
{
|
||||||
|
key: 'kitchen_symbol',
|
||||||
|
title: 'Матовая кухня с островом для семьи в ЖК «Символ»',
|
||||||
|
project_type: 'kitchen',
|
||||||
|
client_name: 'Семья П.',
|
||||||
|
location: 'Москва, ЖК «Символ»',
|
||||||
|
budget: 680000,
|
||||||
|
project_start_at: dateOffset(-95, 8),
|
||||||
|
project_end_at: dateOffset(-72, 17),
|
||||||
|
problem:
|
||||||
|
'Нужно было разместить много хранения и полноценную рабочую поверхность в кухне-гостиной без визуального перегруза.',
|
||||||
|
solution:
|
||||||
|
'Сделали матовые фасады в теплом сером цвете, остров с хранением, высокие пеналы и скрытую подсветку рабочей зоны.',
|
||||||
|
results:
|
||||||
|
'Клиенты получили кухню с удобной посадкой, местом под технику и аккуратным премиальным видом для фото и видео.',
|
||||||
|
is_featured: true,
|
||||||
|
status: 'published',
|
||||||
|
product: productIds.premium_kitchens,
|
||||||
|
importHash: makeImportHash(['case_studies', 'kitchen-symbol']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hallway_wardrobe',
|
||||||
|
title: 'Шкаф-купе до потолка в прихожей с зеркальными фасадами',
|
||||||
|
project_type: 'wardrobe',
|
||||||
|
client_name: 'Антон и Мария С.',
|
||||||
|
location: 'Москва, Хамовники',
|
||||||
|
budget: 245000,
|
||||||
|
project_start_at: dateOffset(-68, 9),
|
||||||
|
project_end_at: dateOffset(-54, 16),
|
||||||
|
problem:
|
||||||
|
'Узкая прихожая быстро захламлялась, а обычный шкаф визуально сужал пространство.',
|
||||||
|
solution:
|
||||||
|
'Собрали шкаф-купе до потолка со светлыми фасадами, зеркалом, секцией под обувь и антресолью под сезонные вещи.',
|
||||||
|
results:
|
||||||
|
'Хранение стало компактным, прихожая визуально расширилась, а клиент отметил удобство ежедневного использования.',
|
||||||
|
is_featured: true,
|
||||||
|
status: 'published',
|
||||||
|
product: productIds.wardrobes_to_ceiling,
|
||||||
|
importHash: makeImportHash(['case_studies', 'hallway-wardrobe']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'master_bedroom_closet',
|
||||||
|
title: 'Гардеробная 8 м² с островной тумбой в мастер-спальне',
|
||||||
|
project_type: 'closet',
|
||||||
|
client_name: 'Екатерина В.',
|
||||||
|
location: 'Московская область, Истра',
|
||||||
|
budget: 430000,
|
||||||
|
project_start_at: dateOffset(-46, 8),
|
||||||
|
project_end_at: dateOffset(-31, 18),
|
||||||
|
problem:
|
||||||
|
'Клиенту нужен был удобный сценарий хранения одежды, обуви и аксессуаров без ощущения тесноты.',
|
||||||
|
solution:
|
||||||
|
'Разделили гардеробную на мужскую и женскую зоны, добавили островную тумбу, подсветку и высокие зеркала.',
|
||||||
|
results:
|
||||||
|
'Комната стала функциональной и визуально лёгкой, а порядок поддерживается без лишних усилий.',
|
||||||
|
is_featured: true,
|
||||||
|
status: 'published',
|
||||||
|
product: productIds.walk_in_closets,
|
||||||
|
importHash: makeImportHash(['case_studies', 'master-bedroom-closet']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'home_office_library',
|
||||||
|
title: 'Домашний кабинет с библиотекой и скрытой проводкой',
|
||||||
|
project_type: 'office',
|
||||||
|
client_name: 'Илья Н.',
|
||||||
|
location: 'Москва, Раменки',
|
||||||
|
budget: 315000,
|
||||||
|
project_start_at: dateOffset(-28, 9),
|
||||||
|
project_end_at: dateOffset(-16, 17),
|
||||||
|
problem:
|
||||||
|
'Кабинет должен был выглядеть статусно на видеозвонках и одновременно быть удобным для ежедневной работы.',
|
||||||
|
solution:
|
||||||
|
'Сделали библиотеку по всей стене, рабочий стол увеличенной глубины, кабель-каналы и акцентную подсветку.',
|
||||||
|
results:
|
||||||
|
'Кабинет выглядит цельно и дорого, а рабочее место стало удобнее для длительной концентрации.',
|
||||||
|
is_featured: false,
|
||||||
|
status: 'published',
|
||||||
|
product: productIds.home_offices,
|
||||||
|
importHash: makeImportHash(['case_studies', 'home-office-library']),
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
const savedCaseStudy = await upsert('case_studies', caseStudies, caseStudyPayload, {
|
||||||
|
matchFields: ['title'],
|
||||||
|
labelFields: ['title'],
|
||||||
|
});
|
||||||
|
|
||||||
|
caseStudyIds[caseStudyPayload.key] = savedCaseStudy.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const briefIds = {};
|
||||||
|
for (const briefPayload of [
|
||||||
|
{
|
||||||
|
key: 'lead_generation_june',
|
||||||
|
title: 'Июнь: заявки на кухни для новостроек',
|
||||||
|
goal: 'lead_generation',
|
||||||
|
audience: 'new_apartments',
|
||||||
|
key_offers:
|
||||||
|
'Бесплатный замер, проект под размеры клиента, монтаж под ключ, подбор материалов под бюджет.',
|
||||||
|
constraints:
|
||||||
|
'Не обещать нереалистичные сроки, не использовать агрессивные скидочные формулировки, держать премиальный тон.',
|
||||||
|
brand_voice: `${organizationName}: спокойный экспертный тон, короткие фразы, акцент на удобстве и процессе.`,
|
||||||
|
call_to_action: 'Напишите в сообщения, чтобы получить расчёт и записаться на замер.',
|
||||||
|
hashtags_seed: 'кухниназаказ мебельназаказ мебельподразмер дизайнкухни замербесплатно',
|
||||||
|
notes:
|
||||||
|
'Нужны посты и reels, которые помогают собрать входящие заявки от клиентов из новых квартир и домов.',
|
||||||
|
status: 'active',
|
||||||
|
owner_user: ownerUserId,
|
||||||
|
importHash: makeImportHash(['content_briefs', 'lead-generation-june']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'behind_scenes_series',
|
||||||
|
title: 'Серия: как создаётся мебель в цехе',
|
||||||
|
goal: 'brand_awareness',
|
||||||
|
audience: 'homeowners',
|
||||||
|
key_offers:
|
||||||
|
'Показать производство, качество фурнитуры, этапы сборки и внимание к деталям.',
|
||||||
|
constraints:
|
||||||
|
'Без личных данных сотрудников и клиентов, без лишнего технического жаргона.',
|
||||||
|
brand_voice: 'Тёплый уверенный тон мастеров, ощущение прозрачности и аккуратного ремесла.',
|
||||||
|
call_to_action: 'Сохраните пост и задайте вопрос по своему проекту в сообщениях.',
|
||||||
|
hashtags_seed: 'мебельныйцех производство мебели столярка мебельназаказ кейсыпомебели',
|
||||||
|
notes:
|
||||||
|
'Подходит для reels и stories: показать процесс, детали, фурнитуру и чистоту производства.',
|
||||||
|
status: 'active',
|
||||||
|
owner_user: ownerUserId,
|
||||||
|
importHash: makeImportHash(['content_briefs', 'behind-scenes-series']),
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
const savedBrief = await upsert('content_briefs', contentBriefs, briefPayload, {
|
||||||
|
matchFields: ['title'],
|
||||||
|
labelFields: ['title'],
|
||||||
|
});
|
||||||
|
|
||||||
|
briefIds[briefPayload.key] = savedBrief.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ideaIds = {};
|
||||||
|
for (const ideaPayload of [
|
||||||
|
{
|
||||||
|
key: 'measurement_checklist',
|
||||||
|
title: 'Чек-лист: что подготовить перед бесплатным замером кухни',
|
||||||
|
format: 'carousel',
|
||||||
|
pillar: 'education',
|
||||||
|
hook: 'Этот чек-лист экономит время и помогает избежать дорогих переделок.',
|
||||||
|
outline:
|
||||||
|
'Показать 5 шагов: размеры помещения, техника, розетки, желаемый стиль, сроки ремонта.',
|
||||||
|
suggested_visuals:
|
||||||
|
'Минималистичный карусельный дизайн, схема кухни, 1 фото реализованного проекта.',
|
||||||
|
suggested_hashtags: 'замеркухни кухниназаказ планированиекуxни',
|
||||||
|
status: 'approved',
|
||||||
|
brief: briefIds.lead_generation_june,
|
||||||
|
importHash: makeImportHash(['content_ideas', 'measurement-checklist']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kitchen_reel',
|
||||||
|
title: 'Reel: кухня с островом — до/после за 25 секунд',
|
||||||
|
format: 'reel',
|
||||||
|
pillar: 'portfolio',
|
||||||
|
hook: 'Из пустой коробки — в кухню, где хочется жить каждый день.',
|
||||||
|
outline:
|
||||||
|
'Короткий таймлайн: пустое помещение, монтаж, подсветка, остров, финальный широкий кадр.',
|
||||||
|
suggested_visuals:
|
||||||
|
'Вертикальное видео 9:16, монтаж с быстрыми склейками, крупные планы фурнитуры и света.',
|
||||||
|
suggested_hashtags: 'допосле кухнясостровом мебельназаказ reel',
|
||||||
|
status: 'approved',
|
||||||
|
brief: briefIds.behind_scenes_series,
|
||||||
|
importHash: makeImportHash(['content_ideas', 'kitchen-reel']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'facades_faq',
|
||||||
|
title: 'Пост: как выбрать материал фасадов без переплаты',
|
||||||
|
format: 'post',
|
||||||
|
pillar: 'faq',
|
||||||
|
hook: 'Разница в фасадах — не только во внешнем виде, но и в сценарии использования.',
|
||||||
|
outline:
|
||||||
|
'Сравнить МДФ в эмали, шпон и HPL по внешнему виду, стойкости, стоимости и уходу.',
|
||||||
|
suggested_visuals:
|
||||||
|
'Карточка-сравнение материалов и фото реальных фасадов крупным планом.',
|
||||||
|
suggested_hashtags: 'фасадыкухни мдфэмаль шпон hpl мебельсоветы',
|
||||||
|
status: 'approved',
|
||||||
|
brief: briefIds.lead_generation_june,
|
||||||
|
importHash: makeImportHash(['content_ideas', 'facades-faq']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'wardrobe_stories',
|
||||||
|
title: 'Stories: 3 причины выбрать шкаф до потолка',
|
||||||
|
format: 'story',
|
||||||
|
pillar: 'faq',
|
||||||
|
hook: 'Когда хранение работает лучше, чем кажется на первый взгляд.',
|
||||||
|
outline:
|
||||||
|
'Показать выгоду по объёму хранения, визуальной чистоте и удобству уборки.',
|
||||||
|
suggested_visuals:
|
||||||
|
'Три коротких вертикальных кадра: общий план, антресоль, внутреннее наполнение.',
|
||||||
|
suggested_hashtags: 'шкафкупе хранениевквартире мебельдляприхожей',
|
||||||
|
status: 'approved',
|
||||||
|
brief: briefIds.lead_generation_june,
|
||||||
|
importHash: makeImportHash(['content_ideas', 'wardrobe-stories']),
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
const savedIdea = await upsert('content_ideas', contentIdeas, ideaPayload, {
|
||||||
|
matchFields: ['title'],
|
||||||
|
labelFields: ['title'],
|
||||||
|
});
|
||||||
|
|
||||||
|
ideaIds[ideaPayload.key] = savedIdea.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const planItemIds = {};
|
||||||
|
for (const planItemPayload of [
|
||||||
|
{
|
||||||
|
key: 'checklist_carousel',
|
||||||
|
content_type: 'carousel',
|
||||||
|
scheduled_at: dateOffset(1, 10),
|
||||||
|
publish_window_end_at: dateOffset(1, 18),
|
||||||
|
status: 'ready',
|
||||||
|
title: 'Карусель: что подготовить к замеру кухни',
|
||||||
|
notes: 'Нужен сильный CTA в последнем слайде: заявка на бесплатный замер.',
|
||||||
|
channel: channels.telegram_channel,
|
||||||
|
idea: ideaIds.measurement_checklist,
|
||||||
|
case_study: caseStudyIds.kitchen_symbol,
|
||||||
|
importHash: makeImportHash(['content_plan_items', 'checklist-carousel']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kitchen_reel_publish',
|
||||||
|
content_type: 'reel',
|
||||||
|
scheduled_at: dateOffset(3, 18, 30),
|
||||||
|
publish_window_end_at: dateOffset(3, 21),
|
||||||
|
status: 'scheduled',
|
||||||
|
title: 'Reel: трансформация кухни с островом',
|
||||||
|
notes: 'Добавить подписи по этапам монтажа и вынести в первые 2 секунды финальный кадр.',
|
||||||
|
channel: channels.instagram_showroom,
|
||||||
|
idea: ideaIds.kitchen_reel,
|
||||||
|
case_study: caseStudyIds.kitchen_symbol,
|
||||||
|
importHash: makeImportHash(['content_plan_items', 'kitchen-reel-publish']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'facades_post',
|
||||||
|
content_type: 'post',
|
||||||
|
scheduled_at: dateOffset(5, 12),
|
||||||
|
publish_window_end_at: dateOffset(5, 19),
|
||||||
|
status: 'planned',
|
||||||
|
title: 'Пост: как выбрать фасады без переплаты',
|
||||||
|
notes: 'Сделать в конце короткий блок “что выбрать под ваш сценарий использования”.',
|
||||||
|
channel: channels.vk_local,
|
||||||
|
idea: ideaIds.facades_faq,
|
||||||
|
case_study: caseStudyIds.kitchen_symbol,
|
||||||
|
importHash: makeImportHash(['content_plan_items', 'facades-post']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'wardrobe_story_pack',
|
||||||
|
content_type: 'story',
|
||||||
|
scheduled_at: dateOffset(7, 17),
|
||||||
|
publish_window_end_at: dateOffset(7, 21),
|
||||||
|
status: 'drafting',
|
||||||
|
title: 'Stories: 3 мифа о шкафах до потолка',
|
||||||
|
notes: 'Финальный сторис с вопросом “рассчитать ваш шкаф?”.',
|
||||||
|
channel: channels.instagram_showroom,
|
||||||
|
idea: ideaIds.wardrobe_stories,
|
||||||
|
case_study: caseStudyIds.hallway_wardrobe,
|
||||||
|
importHash: makeImportHash(['content_plan_items', 'wardrobe-story-pack']),
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
const savedPlanItem = await upsert('content_plan_items', contentPlanItems, planItemPayload, {
|
||||||
|
matchFields: ['title'],
|
||||||
|
labelFields: ['title'],
|
||||||
|
});
|
||||||
|
|
||||||
|
planItemIds[planItemPayload.key] = savedPlanItem.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const generatedContentPayload of [
|
||||||
|
{
|
||||||
|
kind: 'post_caption',
|
||||||
|
language: 'ru',
|
||||||
|
prompt:
|
||||||
|
'Сделай продающий пост о подготовке к замеру кухни на заказ для аудитории новостроек в Москве.',
|
||||||
|
output_text: [
|
||||||
|
'Перед замером кухни важно подготовить не только размеры помещения, но и сценарий жизни семьи.',
|
||||||
|
'Сохраните этот чек-лист: техника, розетки, любимые материалы, сроки ремонта и желаемый стиль.',
|
||||||
|
'Чем точнее вводные, тем быстрее мы соберём проект без лишних переделок и сюрпризов по бюджету.',
|
||||||
|
'Напишите в сообщения — подскажем, что лучше подготовить именно для вашей кухни.',
|
||||||
|
].join('\n\n'),
|
||||||
|
hashtags: '#кухниназаказ #замеркухни #дизайнкухни #мебельподразмер #кухнямечты',
|
||||||
|
status: 'approved',
|
||||||
|
generated_at: dateOffset(-1, 9),
|
||||||
|
brief: briefIds.lead_generation_june,
|
||||||
|
plan_item: planItemIds.checklist_carousel,
|
||||||
|
importHash: makeImportHash(['generated_contents', 'caption-checklist']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'reel_script',
|
||||||
|
language: 'ru',
|
||||||
|
prompt: 'Сделай короткий сценарий reels про кухню с островом: до/после, монтаж, детали, CTA.',
|
||||||
|
output_text: [
|
||||||
|
'Сцена 1: пустое помещение — текст на экране “Когда кухня ещё только в планах”.',
|
||||||
|
'Сцена 2: монтаж пеналов и острова — “Работаем под размеры помещения, а не по шаблону”.',
|
||||||
|
'Сцена 3: крупные планы фурнитуры и подсветки — “Детали, которые ощущаются каждый день”.',
|
||||||
|
'Сцена 4: финальный общий кадр — “Хотите такой же проект? Напишите в сообщения”.',
|
||||||
|
].join('\n\n'),
|
||||||
|
hashtags: '#reelмебель #кухнясостровом #допосле #мебельназаказ',
|
||||||
|
status: 'approved',
|
||||||
|
generated_at: dateOffset(-1, 14),
|
||||||
|
brief: briefIds.behind_scenes_series,
|
||||||
|
plan_item: planItemIds.kitchen_reel_publish,
|
||||||
|
importHash: makeImportHash(['generated_contents', 'reel-kitchen']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'hashtags',
|
||||||
|
language: 'ru',
|
||||||
|
prompt: 'Подбери хэштеги для поста о выборе фасадов кухни без переплаты.',
|
||||||
|
output_text:
|
||||||
|
'Подборка хэштегов для поста о фасадах кухни: сочетание поисковых, локальных и экспертных тегов.',
|
||||||
|
hashtags:
|
||||||
|
'#фасадыкухни #мдфэмаль #шпон #hpl #кухниназаказ #мебельсоветы #ремонтквартиры #дизайнинтерьера',
|
||||||
|
status: 'draft',
|
||||||
|
generated_at: dateOffset(-2, 11),
|
||||||
|
brief: briefIds.lead_generation_june,
|
||||||
|
plan_item: planItemIds.facades_post,
|
||||||
|
importHash: makeImportHash(['generated_contents', 'hashtags-facades']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'dm_reply',
|
||||||
|
language: 'ru',
|
||||||
|
prompt: 'Сделай короткий ответ на входящий лид, который спрашивает стоимость шкафа до потолка.',
|
||||||
|
output_text: [
|
||||||
|
'Здравствуйте! Стоимость шкафа зависит от размеров, наполнения и материалов фасадов.',
|
||||||
|
'Если отправите примерные размеры или фото ниши, мы быстро дадим ориентир и предложим удобный вариант под ваш бюджет.',
|
||||||
|
].join('\n\n'),
|
||||||
|
hashtags: '#ответлиду #продаживдирект',
|
||||||
|
status: 'approved',
|
||||||
|
generated_at: dateOffset(-3, 16),
|
||||||
|
brief: briefIds.lead_generation_june,
|
||||||
|
plan_item: planItemIds.wardrobe_story_pack,
|
||||||
|
importHash: makeImportHash(['generated_contents', 'dm-reply-wardrobe']),
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
await upsert('generated_contents', generatedContents, generatedContentPayload, {
|
||||||
|
matchFields: ['importHash'],
|
||||||
|
labelFields: ['kind', 'importHash'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const leadIds = {};
|
||||||
|
for (const leadPayload of [
|
||||||
|
{
|
||||||
|
key: 'anna_kitchen',
|
||||||
|
source_platform: 'instagram',
|
||||||
|
source_detail: 'Входящий после reels про кухню с островом',
|
||||||
|
full_name: 'Анна Петрова',
|
||||||
|
phone: '+7 999 111-22-33',
|
||||||
|
email: 'anna.pet.demo@example.com',
|
||||||
|
messenger_handle: '@anna_home_project',
|
||||||
|
city: 'Москва',
|
||||||
|
interest_category: 'kitchen',
|
||||||
|
estimated_budget: 650000,
|
||||||
|
requested_at: dateOffset(-1, 8, 30),
|
||||||
|
next_follow_up_at: dateOffset(1, 12),
|
||||||
|
status: 'new',
|
||||||
|
notes:
|
||||||
|
'Квартира в новостройке, нужна кухня с островом и местом под встроенную технику. Просит ориентир по срокам.',
|
||||||
|
assigned_to: ownerUserId,
|
||||||
|
importHash: makeImportHash(['leads', 'anna-kitchen']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ilya_wardrobe',
|
||||||
|
source_platform: 'website',
|
||||||
|
source_detail: 'Форма заявки на странице шкафов-купе',
|
||||||
|
full_name: 'Илья Смирнов',
|
||||||
|
phone: '+7 999 222-33-44',
|
||||||
|
email: 'ilya.wardrobe.demo@example.com',
|
||||||
|
messenger_handle: '@ilya_flat',
|
||||||
|
city: 'Химки',
|
||||||
|
interest_category: 'wardrobe',
|
||||||
|
estimated_budget: 230000,
|
||||||
|
requested_at: dateOffset(-2, 13, 10),
|
||||||
|
next_follow_up_at: dateOffset(1, 15),
|
||||||
|
status: 'contacted',
|
||||||
|
notes:
|
||||||
|
'Интересует шкаф-купе до потолка в прихожую. Уже отправил размеры ниши и хочет понять разницу по материалам.',
|
||||||
|
assigned_to: ownerUserId,
|
||||||
|
importHash: makeImportHash(['leads', 'ilya-wardrobe']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ekaterina_closet',
|
||||||
|
source_platform: 'telegram',
|
||||||
|
source_detail: 'Личный диалог после кейса про гардеробную',
|
||||||
|
full_name: 'Екатерина Волкова',
|
||||||
|
phone: '+7 999 333-44-55',
|
||||||
|
email: 'ekaterina.closet.demo@example.com',
|
||||||
|
messenger_handle: '@ekaterina_style_home',
|
||||||
|
city: 'Истра',
|
||||||
|
interest_category: 'closet',
|
||||||
|
estimated_budget: 420000,
|
||||||
|
requested_at: dateOffset(-4, 10),
|
||||||
|
next_follow_up_at: dateOffset(2, 11),
|
||||||
|
status: 'proposal_sent',
|
||||||
|
notes:
|
||||||
|
'Запрос на гардеробную в мастер-спальне. Коммерческое предложение отправлено, ждём согласование по наполнению.',
|
||||||
|
assigned_to: ownerUserId,
|
||||||
|
importHash: makeImportHash(['leads', 'ekaterina-closet']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'olga_office',
|
||||||
|
source_platform: 'vk',
|
||||||
|
source_detail: 'Сообщение после поста про домашние кабинеты',
|
||||||
|
full_name: 'Ольга Миронова',
|
||||||
|
phone: '+7 999 444-55-66',
|
||||||
|
email: 'olga.office.demo@example.com',
|
||||||
|
messenger_handle: '@olga_remote_work',
|
||||||
|
city: 'Москва',
|
||||||
|
interest_category: 'office',
|
||||||
|
estimated_budget: 310000,
|
||||||
|
requested_at: dateOffset(-5, 9, 40),
|
||||||
|
next_follow_up_at: dateOffset(3, 10),
|
||||||
|
status: 'qualified',
|
||||||
|
notes:
|
||||||
|
'Нужен кабинет с библиотекой и скрытой проводкой. Клиент готов к замеру после согласования сроков ремонта.',
|
||||||
|
assigned_to: ownerUserId,
|
||||||
|
importHash: makeImportHash(['leads', 'olga-office']),
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
const savedLead = await upsert('leads', leads, leadPayload, {
|
||||||
|
matchFields: ['email'],
|
||||||
|
labelFields: ['full_name'],
|
||||||
|
});
|
||||||
|
|
||||||
|
leadIds[leadPayload.key] = savedLead.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const leadActivityPayload of [
|
||||||
|
{
|
||||||
|
activity_type: 'task',
|
||||||
|
occurred_at: null,
|
||||||
|
due_at: dateOffset(1, 12),
|
||||||
|
outcome: 'planned',
|
||||||
|
summary: 'Связаться с Анной и уточнить размеры кухни',
|
||||||
|
details:
|
||||||
|
'Нужно запросить план квартиры, список техники и желаемые сроки установки, затем назначить бесплатный замер.',
|
||||||
|
lead: leadIds.anna_kitchen,
|
||||||
|
author_user: ownerUserId,
|
||||||
|
organizations: organizationId,
|
||||||
|
importHash: makeImportHash(['lead_activities', 'anna-follow-up']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activity_type: 'message',
|
||||||
|
occurred_at: dateOffset(0, 10, 15),
|
||||||
|
due_at: null,
|
||||||
|
outcome: 'done',
|
||||||
|
summary: 'Отправлены варианты наполнения для шкафа Илье',
|
||||||
|
details:
|
||||||
|
'Клиенту отправлены 2 варианта наполнения и пояснение по разнице между ЛДСП и МДФ в эмали.',
|
||||||
|
lead: leadIds.ilya_wardrobe,
|
||||||
|
author_user: ownerUserId,
|
||||||
|
organizations: organizationId,
|
||||||
|
importHash: makeImportHash(['lead_activities', 'ilya-options-sent']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activity_type: 'meeting',
|
||||||
|
occurred_at: null,
|
||||||
|
due_at: dateOffset(2, 11),
|
||||||
|
outcome: 'planned',
|
||||||
|
summary: 'Созвон по КП с Екатериной',
|
||||||
|
details:
|
||||||
|
'Подготовить 2 сценария наполнения гардеробной и пройтись по бюджету перед финальным подтверждением.',
|
||||||
|
lead: leadIds.ekaterina_closet,
|
||||||
|
author_user: ownerUserId,
|
||||||
|
organizations: organizationId,
|
||||||
|
importHash: makeImportHash(['lead_activities', 'ekaterina-proposal-call']),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activity_type: 'call',
|
||||||
|
occurred_at: dateOffset(-1, 17, 20),
|
||||||
|
due_at: null,
|
||||||
|
outcome: 'done',
|
||||||
|
summary: 'Первичный звонок по кабинету для Ольги',
|
||||||
|
details:
|
||||||
|
'Обсудили задачи по хранению документов, видеозвонкам и скрытой проводке. Следующий шаг — замер после готовности стен.',
|
||||||
|
lead: leadIds.olga_office,
|
||||||
|
author_user: ownerUserId,
|
||||||
|
organizations: organizationId,
|
||||||
|
importHash: makeImportHash(['lead_activities', 'olga-intro-call']),
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
await upsert('lead_activities', leadActivities, leadActivityPayload, {
|
||||||
|
matchFields: ['importHash'],
|
||||||
|
labelFields: ['summary'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nDemo data sync complete.');
|
||||||
|
Object.entries(summary).forEach(([resource, stats]) => {
|
||||||
|
console.log(`- ${resource}: created ${stats.created}, updated ${stats.updated}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('Furniture demo data seeding failed.');
|
||||||
|
console.error(error.stack || error.message || error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -87,7 +85,7 @@ module.exports = class Ai_agent_settingsDBApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await ai_agent_settings.setOrganization(currentUser.organization.id || null, {
|
await ai_agent_settings.setOrganization(currentUser.organization?.id || currentUser.organizations?.id || null, {
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -235,7 +233,7 @@ module.exports = class Ai_agent_settingsDBApi {
|
|||||||
if (data.organization !== undefined) {
|
if (data.organization !== undefined) {
|
||||||
await ai_agent_settings.setOrganization(
|
await ai_agent_settings.setOrganization(
|
||||||
|
|
||||||
(globalAccess ? data.organization : currentUser.organization.id),
|
(globalAccess ? data.organization : (currentUser.organization?.id || currentUser.organizations?.id)),
|
||||||
|
|
||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
@ -357,16 +355,13 @@ module.exports = class Ai_agent_settingsDBApi {
|
|||||||
|
|
||||||
if (userOrganizations) {
|
if (userOrganizations) {
|
||||||
if (options?.currentUser?.organizationsId) {
|
if (options?.currentUser?.organizationsId) {
|
||||||
where.organizationsId = options.currentUser.organizationsId;
|
where.organizationId = options.currentUser.organizationsId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -89,7 +87,7 @@ module.exports = class Analytics_snapshotsDBApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await analytics_snapshots.setOrganization(currentUser.organization.id || null, {
|
await analytics_snapshots.setOrganization(currentUser.organization?.id || currentUser.organizations?.id || null, {
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -246,7 +244,7 @@ module.exports = class Analytics_snapshotsDBApi {
|
|||||||
if (data.organization !== undefined) {
|
if (data.organization !== undefined) {
|
||||||
await analytics_snapshots.setOrganization(
|
await analytics_snapshots.setOrganization(
|
||||||
|
|
||||||
(globalAccess ? data.organization : currentUser.organization.id),
|
(globalAccess ? data.organization : (currentUser.organization?.id || currentUser.organizations?.id)),
|
||||||
|
|
||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
@ -382,16 +380,13 @@ module.exports = class Analytics_snapshotsDBApi {
|
|||||||
|
|
||||||
if (userOrganizations) {
|
if (userOrganizations) {
|
||||||
if (options?.currentUser?.organizationsId) {
|
if (options?.currentUser?.organizationsId) {
|
||||||
where.organizationsId = options.currentUser.organizationsId;
|
where.organizationId = options.currentUser.organizationsId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -90,7 +89,7 @@ module.exports = class Case_studiesDBApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await case_studies.setOrganization(currentUser.organization.id || null, {
|
await case_studies.setOrganization(currentUser.organization?.id || currentUser.organizations?.id || null, {
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -292,7 +291,7 @@ module.exports = class Case_studiesDBApi {
|
|||||||
if (data.organization !== undefined) {
|
if (data.organization !== undefined) {
|
||||||
await case_studies.setOrganization(
|
await case_studies.setOrganization(
|
||||||
|
|
||||||
(globalAccess ? data.organization : currentUser.organization.id),
|
(globalAccess ? data.organization : (currentUser.organization?.id || currentUser.organizations?.id)),
|
||||||
|
|
||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
@ -462,16 +461,13 @@ module.exports = class Case_studiesDBApi {
|
|||||||
|
|
||||||
if (userOrganizations) {
|
if (userOrganizations) {
|
||||||
if (options?.currentUser?.organizationsId) {
|
if (options?.currentUser?.organizationsId) {
|
||||||
where.organizationsId = options.currentUser.organizationsId;
|
where.organizationId = options.currentUser.organizationsId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -55,7 +54,7 @@ module.exports = class Content_assetsDBApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await content_assets.setOrganization(currentUser.organization.id || null, {
|
await content_assets.setOrganization(currentUser.organization?.id || currentUser.organizations?.id || null, {
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -201,7 +200,7 @@ module.exports = class Content_assetsDBApi {
|
|||||||
if (data.organization !== undefined) {
|
if (data.organization !== undefined) {
|
||||||
await content_assets.setOrganization(
|
await content_assets.setOrganization(
|
||||||
|
|
||||||
(globalAccess ? data.organization : currentUser.organization.id),
|
(globalAccess ? data.organization : (currentUser.organization?.id || currentUser.organizations?.id)),
|
||||||
|
|
||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
@ -367,16 +366,13 @@ module.exports = class Content_assetsDBApi {
|
|||||||
|
|
||||||
if (userOrganizations) {
|
if (userOrganizations) {
|
||||||
if (options?.currentUser?.organizationsId) {
|
if (options?.currentUser?.organizationsId) {
|
||||||
where.organizationsId = options.currentUser.organizationsId;
|
where.organizationId = options.currentUser.organizationsId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -79,7 +77,7 @@ module.exports = class Content_briefsDBApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await content_briefs.setOrganization(currentUser.organization.id || null, {
|
await content_briefs.setOrganization(currentUser.organization?.id || currentUser.organizations?.id || null, {
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -220,7 +218,7 @@ module.exports = class Content_briefsDBApi {
|
|||||||
if (data.organization !== undefined) {
|
if (data.organization !== undefined) {
|
||||||
await content_briefs.setOrganization(
|
await content_briefs.setOrganization(
|
||||||
|
|
||||||
(globalAccess ? data.organization : currentUser.organization.id),
|
(globalAccess ? data.organization : (currentUser.organization?.id || currentUser.organizations?.id)),
|
||||||
|
|
||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
@ -364,16 +362,13 @@ module.exports = class Content_briefsDBApi {
|
|||||||
|
|
||||||
if (userOrganizations) {
|
if (userOrganizations) {
|
||||||
if (options?.currentUser?.organizationsId) {
|
if (options?.currentUser?.organizationsId) {
|
||||||
where.organizationsId = options.currentUser.organizationsId;
|
where.organizationId = options.currentUser.organizationsId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -69,7 +67,7 @@ module.exports = class Content_ideasDBApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await content_ideas.setOrganization(currentUser.organization.id || null, {
|
await content_ideas.setOrganization(currentUser.organization?.id || currentUser.organizations?.id || null, {
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -194,7 +192,7 @@ module.exports = class Content_ideasDBApi {
|
|||||||
if (data.organization !== undefined) {
|
if (data.organization !== undefined) {
|
||||||
await content_ideas.setOrganization(
|
await content_ideas.setOrganization(
|
||||||
|
|
||||||
(globalAccess ? data.organization : currentUser.organization.id),
|
(globalAccess ? data.organization : (currentUser.organization?.id || currentUser.organizations?.id)),
|
||||||
|
|
||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
@ -334,16 +332,13 @@ module.exports = class Content_ideasDBApi {
|
|||||||
|
|
||||||
if (userOrganizations) {
|
if (userOrganizations) {
|
||||||
if (options?.currentUser?.organizationsId) {
|
if (options?.currentUser?.organizationsId) {
|
||||||
where.organizationsId = options.currentUser.organizationsId;
|
where.organizationId = options.currentUser.organizationsId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -59,7 +57,7 @@ module.exports = class Content_plan_itemsDBApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await content_plan_items.setOrganization(currentUser.organization.id || null, {
|
await content_plan_items.setOrganization(currentUser.organization?.id || currentUser.organizations?.id || null, {
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -176,7 +174,7 @@ module.exports = class Content_plan_itemsDBApi {
|
|||||||
if (data.organization !== undefined) {
|
if (data.organization !== undefined) {
|
||||||
await content_plan_items.setOrganization(
|
await content_plan_items.setOrganization(
|
||||||
|
|
||||||
(globalAccess ? data.organization : currentUser.organization.id),
|
(globalAccess ? data.organization : (currentUser.organization?.id || currentUser.organizations?.id)),
|
||||||
|
|
||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
@ -352,16 +350,13 @@ module.exports = class Content_plan_itemsDBApi {
|
|||||||
|
|
||||||
if (userOrganizations) {
|
if (userOrganizations) {
|
||||||
if (options?.currentUser?.organizationsId) {
|
if (options?.currentUser?.organizationsId) {
|
||||||
where.organizationsId = options.currentUser.organizationsId;
|
where.organizationId = options.currentUser.organizationsId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -64,7 +62,7 @@ module.exports = class Generated_contentsDBApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await generated_contents.setOrganization(currentUser.organization.id || null, {
|
await generated_contents.setOrganization(currentUser.organization?.id || currentUser.organizations?.id || null, {
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -185,7 +183,7 @@ module.exports = class Generated_contentsDBApi {
|
|||||||
if (data.organization !== undefined) {
|
if (data.organization !== undefined) {
|
||||||
await generated_contents.setOrganization(
|
await generated_contents.setOrganization(
|
||||||
|
|
||||||
(globalAccess ? data.organization : currentUser.organization.id),
|
(globalAccess ? data.organization : (currentUser.organization?.id || currentUser.organizations?.id)),
|
||||||
|
|
||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
@ -335,16 +333,13 @@ module.exports = class Generated_contentsDBApi {
|
|||||||
|
|
||||||
if (userOrganizations) {
|
if (userOrganizations) {
|
||||||
if (options?.currentUser?.organizationsId) {
|
if (options?.currentUser?.organizationsId) {
|
||||||
where.organizationsId = options.currentUser.organizationsId;
|
where.organizationId = options.currentUser.organizationsId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -94,7 +92,7 @@ module.exports = class LeadsDBApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await leads.setOrganization(currentUser.organization.id || null, {
|
await leads.setOrganization(currentUser.organization?.id || currentUser.organizations?.id || null, {
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -259,7 +257,7 @@ module.exports = class LeadsDBApi {
|
|||||||
if (data.organization !== undefined) {
|
if (data.organization !== undefined) {
|
||||||
await leads.setOrganization(
|
await leads.setOrganization(
|
||||||
|
|
||||||
(globalAccess ? data.organization : currentUser.organization.id),
|
(globalAccess ? data.organization : (currentUser.organization?.id || currentUser.organizations?.id)),
|
||||||
|
|
||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
@ -399,16 +397,13 @@ module.exports = class LeadsDBApi {
|
|||||||
|
|
||||||
if (userOrganizations) {
|
if (userOrganizations) {
|
||||||
if (options?.currentUser?.organizationsId) {
|
if (options?.currentUser?.organizationsId) {
|
||||||
where.organizationsId = options.currentUser.organizationsId;
|
where.organizationId = options.currentUser.organizationsId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -65,7 +64,7 @@ module.exports = class ProductsDBApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await products.setOrganization(currentUser.organization.id || null, {
|
await products.setOrganization(currentUser.organization?.id || currentUser.organizations?.id || null, {
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -201,7 +200,7 @@ module.exports = class ProductsDBApi {
|
|||||||
if (data.organization !== undefined) {
|
if (data.organization !== undefined) {
|
||||||
await products.setOrganization(
|
await products.setOrganization(
|
||||||
|
|
||||||
(globalAccess ? data.organization : currentUser.organization.id),
|
(globalAccess ? data.organization : (currentUser.organization?.id || currentUser.organizations?.id)),
|
||||||
|
|
||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
@ -342,16 +341,13 @@ module.exports = class ProductsDBApi {
|
|||||||
|
|
||||||
if (userOrganizations) {
|
if (userOrganizations) {
|
||||||
if (options?.currentUser?.organizationsId) {
|
if (options?.currentUser?.organizationsId) {
|
||||||
where.organizationsId = options.currentUser.organizationsId;
|
where.organizationId = options.currentUser.organizationsId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -59,7 +57,7 @@ module.exports = class Publishing_logsDBApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await publishing_logs.setOrganization(currentUser.organization.id || null, {
|
await publishing_logs.setOrganization(currentUser.organization?.id || currentUser.organizations?.id || null, {
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -172,7 +170,7 @@ module.exports = class Publishing_logsDBApi {
|
|||||||
if (data.organization !== undefined) {
|
if (data.organization !== undefined) {
|
||||||
await publishing_logs.setOrganization(
|
await publishing_logs.setOrganization(
|
||||||
|
|
||||||
(globalAccess ? data.organization : currentUser.organization.id),
|
(globalAccess ? data.organization : (currentUser.organization?.id || currentUser.organizations?.id)),
|
||||||
|
|
||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
@ -322,16 +320,13 @@ module.exports = class Publishing_logsDBApi {
|
|||||||
|
|
||||||
if (userOrganizations) {
|
if (userOrganizations) {
|
||||||
if (options?.currentUser?.organizationsId) {
|
if (options?.currentUser?.organizationsId) {
|
||||||
where.organizationsId = options.currentUser.organizationsId;
|
where.organizationId = options.currentUser.organizationsId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -60,7 +58,7 @@ module.exports = class Social_channelsDBApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
await social_channels.setOrganization(currentUser.organization.id || null, {
|
await social_channels.setOrganization(currentUser.organization?.id || currentUser.organizations?.id || null, {
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -166,7 +164,7 @@ module.exports = class Social_channelsDBApi {
|
|||||||
if (data.organization !== undefined) {
|
if (data.organization !== undefined) {
|
||||||
await social_channels.setOrganization(
|
await social_channels.setOrganization(
|
||||||
|
|
||||||
(globalAccess ? data.organization : currentUser.organization.id),
|
(globalAccess ? data.organization : (currentUser.organization?.id || currentUser.organizations?.id)),
|
||||||
|
|
||||||
{ transaction }
|
{ transaction }
|
||||||
);
|
);
|
||||||
@ -300,16 +298,13 @@ module.exports = class Social_channelsDBApi {
|
|||||||
|
|
||||||
if (userOrganizations) {
|
if (userOrganizations) {
|
||||||
if (options?.currentUser?.organizationsId) {
|
if (options?.currentUser?.organizationsId) {
|
||||||
where.organizationsId = options.currentUser.organizationsId;
|
where.organizationId = options.currentUser.organizationsId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,8 @@ import { mdiLogout, mdiClose } from '@mdi/js'
|
|||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
import AsideMenuList from './AsideMenuList'
|
import AsideMenuList from './AsideMenuList'
|
||||||
import { MenuAsideItem } from '../interfaces'
|
import { MenuAsideItem } from '../interfaces'
|
||||||
import { useAppSelector } from '../stores/hooks'
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
import { useAppDispatch } from '../stores/hooks';
|
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useRef} from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
import BaseDivider from './BaseDivider'
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react'
|
import React, { ReactNode, useEffect, useState } from 'react'
|
||||||
import { useState } from 'react'
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
|
|||||||
@ -7,6 +7,12 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/agent-studio',
|
||||||
|
label: 'Agent Studio',
|
||||||
|
icon: 'mdiRobotOutline' in icon ? icon['mdiRobotOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
|
permissions: ['CREATE_CONTENT_BRIEFS', 'CREATE_GENERATED_CONTENTS', 'CREATE_CONTENT_PLAN_ITEMS', 'READ_GENERATED_CONTENTS']
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
href: '/users/users-list',
|
||||||
|
|||||||
2197
frontend/src/pages/agent-studio.tsx
Normal file
2197
frontend/src/pages/agent-studio.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,166 +1,291 @@
|
|||||||
|
import {
|
||||||
import React, { useEffect, useState } from 'react';
|
mdiAccountArrowRight,
|
||||||
import type { ReactElement } from 'react';
|
mdiArrowRight,
|
||||||
|
mdiArrowTopRight,
|
||||||
|
mdiBullhornOutline,
|
||||||
|
mdiCalendarMonth,
|
||||||
|
mdiChartTimelineVariant,
|
||||||
|
mdiCheckCircleOutline,
|
||||||
|
mdiRobotOutline,
|
||||||
|
} from '@mdi/js';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import type { ReactElement } from 'react';
|
||||||
|
import React from 'react';
|
||||||
import BaseButton from '../components/BaseButton';
|
import BaseButton from '../components/BaseButton';
|
||||||
import CardBox from '../components/CardBox';
|
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
|
||||||
import LayoutGuest from '../layouts/Guest';
|
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import BaseButtons from '../components/BaseButtons';
|
import BaseButtons from '../components/BaseButtons';
|
||||||
|
import BaseIcon from '../components/BaseIcon';
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
|
||||||
|
|
||||||
|
const featureCards = [
|
||||||
|
{
|
||||||
|
icon: mdiCalendarMonth,
|
||||||
|
title: 'Контент-календарь',
|
||||||
|
description: 'Планируйте посты, Stories и Reels в едином ритме, а не в формате «вспомнили — опубликовали».',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: mdiRobotOutline,
|
||||||
|
title: 'AI-тексты и идеи',
|
||||||
|
description: 'Получайте готовые подписи, офферы, хэштеги и заготовки для диалогов в директе на базе ваших кейсов и услуг.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: mdiAccountArrowRight,
|
||||||
|
title: 'Лиды без потерь',
|
||||||
|
description: 'Фиксируйте входящие заявки из соцсетей и сразу видьте, что нужно сделать дальше по каждому клиенту.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function Starter() {
|
const workflowSteps = [
|
||||||
const [illustrationImage, setIllustrationImage] = useState({
|
{
|
||||||
src: undefined,
|
step: '01',
|
||||||
photographer: undefined,
|
title: 'Соберите контекст бренда',
|
||||||
photographer_url: undefined,
|
text: 'Кейсы, продукты, каналы и офферы создают базу, на которой AI пишет не «как у всех», а про ваш мебельный бизнес.',
|
||||||
})
|
},
|
||||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
{
|
||||||
const [contentType, setContentType] = useState('video');
|
step: '02',
|
||||||
const [contentPosition, setContentPosition] = useState('left');
|
title: 'Запустите Agent Studio',
|
||||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
text: 'Опишите задачу кампании и получите caption, Stories-структуру, хэштеги и ответ для тёплого лида.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
step: '03',
|
||||||
|
title: 'Сохраните в рабочий поток',
|
||||||
|
text: 'Черновик, бриф и позиция в контент-плане сохраняются в систему, чтобы маркетинг, контент и лиды были в одном месте.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const title = 'Furniture SMM AI Agent'
|
const controlCards = [
|
||||||
|
{
|
||||||
// Fetch Pexels image/video
|
icon: mdiRobotOutline,
|
||||||
useEffect(() => {
|
title: 'Agent Studio',
|
||||||
async function fetchData() {
|
description: 'Новая рабочая зона для быстрого запуска AI-контента и сохранения результата в CRM.',
|
||||||
const image = await getPexelsImage();
|
href: '/agent-studio',
|
||||||
const video = await getPexelsVideo();
|
action: 'Открыть workflow',
|
||||||
setIllustrationImage(image);
|
},
|
||||||
setIllustrationVideo(video);
|
{
|
||||||
}
|
icon: mdiChartTimelineVariant,
|
||||||
fetchData();
|
title: 'Админ-интерфейс',
|
||||||
}, []);
|
description: 'Авторизация, сущности, календарь контента, лиды и аналитика уже доступны из SaaS-панели.',
|
||||||
|
href: '/login',
|
||||||
const imageBlock = (image) => (
|
action: 'Войти в админку',
|
||||||
<div
|
},
|
||||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
{
|
||||||
style={{
|
icon: mdiBullhornOutline,
|
||||||
backgroundImage: `${
|
title: 'Контент и лиды',
|
||||||
image
|
description: 'Подходит владельцу или маркетологу мебельного производства, которому нужен регулярный SMM без хаоса.',
|
||||||
? `url(${image?.src?.original})`
|
href: '/login',
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
action: 'Запустить кабинет',
|
||||||
}`,
|
},
|
||||||
backgroundSize: 'cover',
|
];
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
|
||||||
<a
|
|
||||||
className='text-[8px]'
|
|
||||||
href={image?.photographer_url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Photo by {image?.photographer} on Pexels
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const videoBlock = (video) => {
|
|
||||||
if (video?.video_files?.length > 0) {
|
|
||||||
return (
|
|
||||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
|
||||||
<video
|
|
||||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
|
||||||
autoPlay
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
>
|
|
||||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
|
||||||
<a
|
|
||||||
className='text-[8px]'
|
|
||||||
href={video?.user?.url}
|
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
|
||||||
>
|
|
||||||
Video by {video.user.name} on Pexels
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
style={
|
|
||||||
contentPosition === 'background'
|
|
||||||
? {
|
|
||||||
backgroundImage: `${
|
|
||||||
illustrationImage
|
|
||||||
? `url(${illustrationImage.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Starter Page')}</title>
|
<title>{getPageTitle('Furniture marketing AI')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<div className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(37,99,235,0.12),_transparent_32%),linear-gradient(180deg,#f8fafc_0%,#eef2ff_45%,#ffffff_100%)] text-slate-900">
|
||||||
<div
|
<header className="sticky top-0 z-20 border-b border-slate-200/80 bg-white/80 backdrop-blur">
|
||||||
className={`flex ${
|
<div className="mx-auto flex w-full max-w-7xl items-center justify-between px-6 py-4 lg:px-8">
|
||||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
<div>
|
||||||
} min-h-screen w-full`}
|
<p className="text-sm font-semibold uppercase tracking-[0.28em] text-blue-700">Furniture SMM AI Agent</p>
|
||||||
>
|
<p className="mt-1 text-sm text-slate-500">ИИ-агент и мини-CRM для мебельного производства на заказ</p>
|
||||||
{contentType === 'image' && contentPosition !== 'background'
|
|
||||||
? imageBlock(illustrationImage)
|
|
||||||
: null}
|
|
||||||
{contentType === 'video' && contentPosition !== 'background'
|
|
||||||
? videoBlock(illustrationVideo)
|
|
||||||
: null}
|
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
|
||||||
<CardBoxComponentTitle title="Welcome to your Furniture SMM AI Agent app!"/>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
|
||||||
<p className='text-center text-gray-500'>For guides and documentation please check
|
|
||||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="hidden items-center gap-8 lg:flex">
|
||||||
<BaseButtons>
|
<a href="#features" className="text-sm font-medium text-slate-600 transition hover:text-slate-900">
|
||||||
<BaseButton
|
Возможности
|
||||||
href='/login'
|
</a>
|
||||||
label='Login'
|
<a href="#workflow" className="text-sm font-medium text-slate-600 transition hover:text-slate-900">
|
||||||
color='info'
|
Workflow
|
||||||
className='w-full'
|
</a>
|
||||||
/>
|
<a href="#workspace" className="text-sm font-medium text-slate-600 transition hover:text-slate-900">
|
||||||
|
Рабочая зона
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<BaseButtons mb="mb-0" classAddon="mr-2 last:mr-0 mb-0" noWrap>
|
||||||
|
<BaseButton href="/agent-studio" label="Agent Studio" color="lightDark" />
|
||||||
|
<BaseButton href="/login" label="Войти в админку" color="info" />
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</CardBox>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
</div>
|
|
||||||
</SectionFullScreen>
|
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
|
||||||
Privacy Policy
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<main>
|
||||||
|
<section className="mx-auto grid w-full max-w-7xl gap-12 px-6 py-16 lg:grid-cols-[1.1fr,0.9fr] lg:px-8 lg:py-24">
|
||||||
|
<div>
|
||||||
|
<div className="inline-flex items-center rounded-full border border-blue-100 bg-blue-50 px-4 py-2 text-sm font-semibold text-blue-700">
|
||||||
|
<BaseIcon path={mdiCheckCircleOutline} size={18} className="mr-2" />
|
||||||
|
Первый MVP-срез уже включает рабочий контент-flow внутри панели
|
||||||
|
</div>
|
||||||
|
<h1 className="mt-6 max-w-4xl text-5xl font-semibold tracking-tight text-slate-950 sm:text-6xl">
|
||||||
|
Премиальный SMM-агент для бизнеса на мебели под заказ — от идеи поста до сохранённого контент-плана.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-6 max-w-2xl text-lg leading-8 text-slate-600">
|
||||||
|
Приложение помогает владельцу или маркетологу регулярно выпускать контент, генерировать входящие заявки и не терять
|
||||||
|
лиды из Instagram, Telegram и других каналов. Всё — в едином аккуратном кабинете.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<BaseButtons className="mt-8" type="justify-start" mb="mb-0" classAddon="mr-3 last:mr-0 mb-3">
|
||||||
|
<BaseButton href="/login" label="Войти в админку" color="info" icon={mdiArrowRight} />
|
||||||
|
<BaseButton href="/agent-studio" label="Открыть Agent Studio" color="lightDark" icon={mdiRobotOutline} />
|
||||||
|
</BaseButtons>
|
||||||
|
|
||||||
|
<div className="mt-10 grid gap-4 sm:grid-cols-3">
|
||||||
|
<div className="rounded-3xl border border-slate-200 bg-white/80 p-5 shadow-sm">
|
||||||
|
<p className="text-sm text-slate-500">Контент-поток</p>
|
||||||
|
<p className="mt-3 text-3xl font-semibold tracking-tight text-slate-900">AI → План</p>
|
||||||
|
<p className="mt-3 text-sm leading-6 text-slate-500">Черновик, бриф и запись в календарь сохраняются последовательно.</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-3xl border border-slate-200 bg-white/80 p-5 shadow-sm">
|
||||||
|
<p className="text-sm text-slate-500">Лид-ориентация</p>
|
||||||
|
<p className="mt-3 text-3xl font-semibold tracking-tight text-slate-900">DM reply</p>
|
||||||
|
<p className="mt-3 text-sm leading-6 text-slate-500">Для каждой публикации можно сразу получить ответ на тёплый входящий запрос.</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-3xl border border-slate-200 bg-white/80 p-5 shadow-sm">
|
||||||
|
<p className="text-sm text-slate-500">Под мебельный бренд</p>
|
||||||
|
<p className="mt-3 text-3xl font-semibold tracking-tight text-slate-900">Clean / premium</p>
|
||||||
|
<p className="mt-3 text-sm leading-6 text-slate-500">Тон, офферы, кейсы и продукты делают AI-контент ближе к реальному бизнесу.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute -right-8 -top-10 h-40 w-40 rounded-full bg-cyan-200/60 blur-3xl" />
|
||||||
|
<div className="absolute -left-10 bottom-10 h-44 w-44 rounded-full bg-blue-200/60 blur-3xl" />
|
||||||
|
<div className="relative overflow-hidden rounded-[32px] border border-slate-200 bg-slate-950 p-6 text-white shadow-2xl">
|
||||||
|
<div className="rounded-[28px] border border-white/10 bg-white/5 p-5 backdrop-blur">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-blue-200">Agent Studio</p>
|
||||||
|
<h2 className="mt-3 text-2xl font-semibold">Готовый рабочий сценарий для контент-маркетинга</h2>
|
||||||
|
</div>
|
||||||
|
<span className="rounded-full border border-white/10 bg-white/10 px-3 py-1 text-xs font-semibold text-cyan-200">
|
||||||
|
MVP Slice
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 space-y-3">
|
||||||
|
<div className="rounded-2xl border border-white/10 bg-white/10 p-4">
|
||||||
|
<p className="text-sm font-medium text-slate-300">1. Бриф кампании</p>
|
||||||
|
<p className="mt-2 text-lg font-semibold">Кухни под новые квартиры</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border border-white/10 bg-white/10 p-4">
|
||||||
|
<p className="text-sm font-medium text-slate-300">2. AI-выгрузка</p>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-slate-200">
|
||||||
|
Caption, Stories, хэштеги, визуальный бриф и ответ для тёплого лида — в одном экране.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border border-white/10 bg-white/10 p-4">
|
||||||
|
<p className="text-sm font-medium text-slate-300">3. Сохранение</p>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-slate-200">
|
||||||
|
Created: Content brief → Generated content → Content plan item.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid gap-4 sm:grid-cols-3">
|
||||||
|
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
|
||||||
|
<p className="text-sm text-slate-300">Контент-план</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold">Планомерно</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
|
||||||
|
<p className="text-sm text-slate-300">AI-черновики</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold">Быстрее</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
|
||||||
|
<p className="text-sm text-slate-300">Лиды</p>
|
||||||
|
<p className="mt-2 text-2xl font-semibold">Без потерь</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="features" className="mx-auto w-full max-w-7xl px-6 py-8 lg:px-8 lg:py-12">
|
||||||
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold uppercase tracking-[0.24em] text-blue-700">Capabilities</p>
|
||||||
|
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-slate-950">Что уже закрывает первый релиз</h2>
|
||||||
|
</div>
|
||||||
|
<p className="max-w-2xl text-sm leading-7 text-slate-500">
|
||||||
|
Не просто красивый лендинг: уже сейчас есть конкретный рабочий сценарий для маркетолога мебельной компании.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 grid gap-5 lg:grid-cols-3">
|
||||||
|
{featureCards.map((feature) => (
|
||||||
|
<div key={feature.title} className="rounded-[28px] border border-slate-200 bg-white p-6 shadow-sm">
|
||||||
|
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-blue-50 text-blue-700">
|
||||||
|
<BaseIcon path={feature.icon} size={26} />
|
||||||
|
</div>
|
||||||
|
<h3 className="mt-5 text-xl font-semibold text-slate-900">{feature.title}</h3>
|
||||||
|
<p className="mt-3 text-sm leading-7 text-slate-500">{feature.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="workflow" className="mx-auto w-full max-w-7xl px-6 py-8 lg:px-8 lg:py-12">
|
||||||
|
<div className="overflow-hidden rounded-[32px] border border-slate-200 bg-white p-8 shadow-sm lg:p-10">
|
||||||
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold uppercase tracking-[0.24em] text-blue-700">Workflow</p>
|
||||||
|
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-slate-950">Тонкий end-to-end слой вместо очередной пустой витрины</h2>
|
||||||
|
</div>
|
||||||
|
<p className="max-w-2xl text-sm leading-7 text-slate-500">
|
||||||
|
Главная ценность первого MVP — реальная цепочка действий внутри продукта, а не просто список сущностей.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 grid gap-5 lg:grid-cols-3">
|
||||||
|
{workflowSteps.map((item) => (
|
||||||
|
<div key={item.step} className="rounded-3xl border border-slate-200 bg-slate-50 p-6">
|
||||||
|
<p className="text-sm font-semibold tracking-[0.2em] text-blue-700">{item.step}</p>
|
||||||
|
<h3 className="mt-4 text-xl font-semibold text-slate-900">{item.title}</h3>
|
||||||
|
<p className="mt-3 text-sm leading-7 text-slate-500">{item.text}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="workspace" className="mx-auto w-full max-w-7xl px-6 py-8 lg:px-8 lg:py-12">
|
||||||
|
<div className="grid gap-5 lg:grid-cols-3">
|
||||||
|
{controlCards.map((card) => (
|
||||||
|
<a
|
||||||
|
key={card.title}
|
||||||
|
href={card.href}
|
||||||
|
className="group rounded-[28px] border border-slate-200 bg-white p-6 shadow-sm transition hover:-translate-y-0.5 hover:border-blue-300"
|
||||||
|
>
|
||||||
|
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-slate-950 text-white">
|
||||||
|
<BaseIcon path={card.icon} size={24} />
|
||||||
|
</div>
|
||||||
|
<h3 className="mt-5 text-xl font-semibold text-slate-900">{card.title}</h3>
|
||||||
|
<p className="mt-3 text-sm leading-7 text-slate-500">{card.description}</p>
|
||||||
|
<div className="mt-6 inline-flex items-center text-sm font-semibold text-blue-700">
|
||||||
|
{card.action}
|
||||||
|
<BaseIcon path={mdiArrowTopRight} size={18} className="ml-2 transition group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="border-t border-slate-200 bg-white/70">
|
||||||
|
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-6 py-8 text-sm text-slate-500 lg:flex-row lg:items-center lg:justify-between lg:px-8">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-slate-900">Furniture SMM AI Agent</p>
|
||||||
|
<p className="mt-1">Публичный лендинг + рабочий вход в админку и Agent Studio.</p>
|
||||||
|
</div>
|
||||||
|
<BaseButtons mb="mb-0" classAddon="mr-2 last:mr-0 mb-0" noWrap>
|
||||||
|
<BaseButton href="/agent-studio" label="Agent Studio" color="lightDark" />
|
||||||
|
<BaseButton href="/login" label="Login / Admin" color="info" />
|
||||||
|
</BaseButtons>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
Home.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import React, { ReactElement, useEffect, useState } from 'react';
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
import { useAppDispatch } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
import { useAppSelector } from '../stores/hooks';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user