forked from admin/zeropost-engine
feat: /api/calendar endpoint (user_posts + scheduled_posts)
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user