161 lines
6.3 KiB
JavaScript
161 lines
6.3 KiB
JavaScript
/**
|
|
* GET /api/calendar
|
|
* Возвращает объединённый список событий за период:
|
|
* - user_posts (черновики / запланированные / опубликованные пользователем)
|
|
* - scheduled_posts (авто-публикации статей блога)
|
|
*
|
|
* Query params:
|
|
* from — ISO date начала (включительно), default = начало текущего месяца
|
|
* to — ISO date конца (включительно), default = конец следующего месяца
|
|
* channel_id — фильтр по каналу (опционально)
|
|
* user_id — берётся из заголовка x-user-id
|
|
*/
|
|
|
|
const express = require('express');
|
|
const router = express.Router();
|
|
const { query } = require('../config/db');
|
|
|
|
const getUserId = (req) => parseInt(req.headers['x-user-id']) || null;
|
|
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const userId = getUserId(req);
|
|
if (!userId) return res.status(401).json({ error: 'Unauthorized' });
|
|
|
|
// Диапазон дат
|
|
const now = new Date();
|
|
const defaultFrom = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
const defaultTo = new Date(now.getFullYear(), now.getMonth() + 2, 0, 23, 59, 59);
|
|
|
|
const from = req.query.from ? new Date(req.query.from) : defaultFrom;
|
|
const to = req.query.to ? new Date(req.query.to) : defaultTo;
|
|
const channelId = req.query.channel_id ? parseInt(req.query.channel_id) : null;
|
|
|
|
const events = [];
|
|
|
|
// ── user_posts ────────────────────────────────────────────────────────────
|
|
// Берём посты юзера у которых scheduled_at или published_at попадает в диапазон
|
|
const upParams = [userId, from, to];
|
|
let upWhere = `up.user_id = $1
|
|
AND (
|
|
(up.scheduled_at IS NOT NULL AND up.scheduled_at BETWEEN $2 AND $3)
|
|
OR
|
|
(up.published_at IS NOT NULL AND up.published_at BETWEEN $2 AND $3)
|
|
OR
|
|
(up.scheduled_at IS NULL AND up.published_at IS NULL AND up.status='draft'
|
|
AND up.created_at BETWEEN $2 AND $3)
|
|
)`;
|
|
|
|
if (channelId) {
|
|
upParams.push(channelId);
|
|
upWhere += ` AND up.channel_id = $${upParams.length}`;
|
|
}
|
|
|
|
const { rows: userPosts } = await query(
|
|
`SELECT up.id, up.channel_id, up.status,
|
|
up.scheduled_at, up.published_at, up.created_at,
|
|
left(up.content, 120) AS content_preview,
|
|
up.topic,
|
|
up.image_url,
|
|
c.name AS channel_name, c.platform
|
|
FROM user_posts up
|
|
JOIN channels c ON c.id = up.channel_id
|
|
WHERE ${upWhere}
|
|
ORDER BY COALESCE(up.scheduled_at, up.published_at, up.created_at) ASC
|
|
LIMIT 500`,
|
|
upParams
|
|
);
|
|
|
|
for (const p of userPosts) {
|
|
events.push({
|
|
id: `up_${p.id}`,
|
|
source: 'user_post',
|
|
source_id: p.id,
|
|
channel_id: p.channel_id,
|
|
channel_name: p.channel_name,
|
|
platform: p.platform,
|
|
status: p.status, // draft | scheduled | published | failed
|
|
date: p.scheduled_at || p.published_at || p.created_at,
|
|
scheduled_at: p.scheduled_at,
|
|
published_at: p.published_at,
|
|
title: p.topic || null,
|
|
preview: p.content_preview,
|
|
image_url: p.image_url,
|
|
editable: true, // можно перетаскивать / редактировать
|
|
});
|
|
}
|
|
|
|
// ── scheduled_posts (авто-публикации статей блога) ────────────────────────
|
|
// Отдаём только если нет фильтра по каналу, или канал совпадает
|
|
const spParams = [from, to];
|
|
let spWhere = `sp.scheduled_at BETWEEN $1 AND $2`;
|
|
|
|
if (channelId) {
|
|
spParams.push(channelId);
|
|
spWhere += ` AND sp.channel_id = $${spParams.length}`;
|
|
} else {
|
|
// Только каналы этого юзера (или системные)
|
|
spParams.push(userId);
|
|
spWhere += ` AND (c.user_id = $${spParams.length} OR c.is_system = true)`;
|
|
}
|
|
|
|
const { rows: schedPosts } = await query(
|
|
`SELECT sp.id, sp.channel_id, sp.status,
|
|
sp.scheduled_at, sp.published_at,
|
|
sp.error,
|
|
a.title AS article_title, a.slug AS article_slug,
|
|
a.category AS article_category, a.cover_url,
|
|
c.name AS channel_name, c.platform
|
|
FROM scheduled_posts sp
|
|
JOIN channels c ON c.id = sp.channel_id
|
|
LEFT JOIN articles a ON a.id = sp.article_id
|
|
WHERE ${spWhere}
|
|
ORDER BY sp.scheduled_at ASC
|
|
LIMIT 500`,
|
|
spParams
|
|
);
|
|
|
|
for (const p of schedPosts) {
|
|
// Маппинг статусов: pending → scheduled, sent → published
|
|
const status = p.status === 'sent' ? 'published'
|
|
: p.status === 'pending' ? 'scheduled'
|
|
: p.status; // failed
|
|
|
|
events.push({
|
|
id: `sp_${p.id}`,
|
|
source: 'scheduled_post',
|
|
source_id: p.id,
|
|
channel_id: p.channel_id,
|
|
channel_name: p.channel_name,
|
|
platform: p.platform,
|
|
status,
|
|
date: p.published_at || p.scheduled_at,
|
|
scheduled_at: p.scheduled_at,
|
|
published_at: p.published_at,
|
|
title: p.article_title || null,
|
|
preview: p.article_title || null,
|
|
image_url: p.cover_url || null,
|
|
article_slug: p.article_slug || null,
|
|
article_category: p.article_category || null,
|
|
error: p.error || null,
|
|
editable: p.status === 'pending', // pending можно отменить/перенести
|
|
});
|
|
}
|
|
|
|
// Сортируем общий список по дате
|
|
events.sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
|
|
res.json({
|
|
from: from.toISOString(),
|
|
to: to.toISOString(),
|
|
count: events.length,
|
|
events,
|
|
});
|
|
} catch (err) {
|
|
console.error('[Calendar] Error:', err);
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|