diff --git a/app/api/channels/[channelId]/drafts/generate/route.js b/app/api/channels/[channelId]/drafts/generate/route.js new file mode 100644 index 0000000..355db21 --- /dev/null +++ b/app/api/channels/[channelId]/drafts/generate/route.js @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server'; +import { requireUser } from '@/lib/session'; + +const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030'; +const ENGINE_SECRET = process.env.ENGINE_SECRET || ''; + +export async function POST(req, { params }) { + const user = await requireUser(); + if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const { searchParams } = new URL(req.url); + const body = await req.json().catch(() => ({})); + const res = await fetch( + `${ENGINE_URL}/api/channels/${params.channelId}/drafts/generate?count=${searchParams.get('count') || body.count || 3}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(user.id) }, + body: JSON.stringify(body), + } + ); + return NextResponse.json(await res.json()); +} diff --git a/app/api/drafts/[id]/approve/route.js b/app/api/drafts/[id]/approve/route.js new file mode 100644 index 0000000..07d53eb --- /dev/null +++ b/app/api/drafts/[id]/approve/route.js @@ -0,0 +1,17 @@ +import { NextResponse } from 'next/server'; +import { requireUser } from '@/lib/session'; + +const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030'; +const ENGINE_SECRET = process.env.ENGINE_SECRET || ''; + +export async function POST(req, { params }) { + const user = await requireUser(); + if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const body = await req.json().catch(() => ({})); + const res = await fetch(`${ENGINE_URL}/api/drafts/${params.id}/approve`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(user.id) }, + body: JSON.stringify(body), + }); + return NextResponse.json(await res.json()); +} diff --git a/app/api/drafts/[id]/reject/route.js b/app/api/drafts/[id]/reject/route.js new file mode 100644 index 0000000..f65bd20 --- /dev/null +++ b/app/api/drafts/[id]/reject/route.js @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; +import { requireUser } from '@/lib/session'; + +const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030'; +const ENGINE_SECRET = process.env.ENGINE_SECRET || ''; + +export async function POST(req, { params }) { + const user = await requireUser(); + if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const res = await fetch(`${ENGINE_URL}/api/drafts/${params.id}/reject`, { + method: 'POST', + headers: { 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(user.id) }, + }); + return NextResponse.json(await res.json()); +} diff --git a/app/api/drafts/[id]/route.js b/app/api/drafts/[id]/route.js new file mode 100644 index 0000000..ad5afd1 --- /dev/null +++ b/app/api/drafts/[id]/route.js @@ -0,0 +1,30 @@ +import { NextResponse } from 'next/server'; +import { requireUser } from '@/lib/session'; + +const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030'; +const ENGINE_SECRET = process.env.ENGINE_SECRET || ''; + +function h(userId) { + return { 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(userId) }; +} + +// PATCH /api/drafts/:id +export async function PATCH(req, { params }) { + const user = await requireUser(); + if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const body = await req.json(); + const res = await fetch(`${ENGINE_URL}/api/drafts/${params.id}`, { + method: 'PATCH', + headers: { ...h(user.id), 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + return NextResponse.json(await res.json()); +} + +// DELETE /api/drafts/:id +export async function DELETE(req, { params }) { + const user = await requireUser(); + if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const res = await fetch(`${ENGINE_URL}/api/drafts/${params.id}`, { method: 'DELETE', headers: h(user.id) }); + return NextResponse.json(await res.json()); +} diff --git a/app/api/drafts/route.js b/app/api/drafts/route.js new file mode 100644 index 0000000..0db94d8 --- /dev/null +++ b/app/api/drafts/route.js @@ -0,0 +1,18 @@ +import { NextResponse } from 'next/server'; +import { requireUser } from '@/lib/session'; + +const ENGINE_URL = process.env.ENGINE_URL || 'http://127.0.0.1:3030'; +const ENGINE_SECRET = process.env.ENGINE_SECRET || ''; + +function eHeaders(userId) { + return { 'x-internal-secret': ENGINE_SECRET, 'x-user-id': String(userId) }; +} + +// GET /api/drafts — все черновики пользователя +export async function GET(req) { + const user = await requireUser(); + if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const { searchParams } = new URL(req.url); + const res = await fetch(`${ENGINE_URL}/api/drafts?${searchParams}`, { headers: eHeaders(user.id) }); + return NextResponse.json(await res.json()); +} diff --git a/app/drafts/page.js b/app/drafts/page.js new file mode 100644 index 0000000..776f2d9 --- /dev/null +++ b/app/drafts/page.js @@ -0,0 +1,223 @@ +'use client'; +import { useState, useEffect, useCallback } from 'react'; +import { Clock, Check, X, Edit3, Trash2, RefreshCw, Loader2, Calendar, Image as ImgIcon, Zap } from 'lucide-react'; +import Link from 'next/link'; + +const STATUS_TABS = [ + { v: 'pending', label: 'Ожидают', color: 'text-accent' }, + { v: 'approved', label: 'Одобрено', color: 'text-green-400' }, + { v: 'rejected', label: 'Отклонено', color: 'text-gray-500' }, +]; + +function timeAgo(s) { + const d = new Date(s), now = new Date(); + const diff = now - d; + if (diff < 3600000) return Math.floor(diff / 60000) + ' мин назад'; + if (diff < 86400000) return Math.floor(diff / 3600000) + 'ч назад'; + return d.toLocaleDateString('ru-RU'); +} + +export default function DraftsPage() { + const [tab, setTab] = useState('pending'); + const [drafts, setDrafts] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading]= useState(true); + const [editing, setEditing]= useState(null); // draft id + const [editText,setEditText]=useState(''); + const [schedMap,setSchedMap]=useState({}); // draftId → scheduledAt + const [busy, setBusy] = useState({}); + + const load = useCallback(async (t = tab) => { + setLoading(true); + try { + const res = await fetch(`/api/drafts?status=${t}&limit=50`).then(r => r.json()); + setDrafts(res.drafts || []); + setTotal(res.total || 0); + } catch {} + setLoading(false); + }, [tab]); + + useEffect(() => { load(tab); }, [tab]); + + async function doApprove(id) { + setBusy(b => ({ ...b, [id]: true })); + const res = await fetch(`/api/drafts/${id}/approve`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ scheduled_at: schedMap[id] || null }), + }).then(r => r.json()); + setBusy(b => ({ ...b, [id]: false })); + if (res.ok) load(tab); else alert(res.error); + } + + async function doReject(id) { + setBusy(b => ({ ...b, [id]: true })); + await fetch(`/api/drafts/${id}/reject`, { method: 'POST' }); + setBusy(b => ({ ...b, [id]: false })); + load(tab); + } + + async function doDelete(id) { + if (!confirm('Удалить черновик?')) return; + await fetch(`/api/drafts/${id}`, { method: 'DELETE' }); + load(tab); + } + + async function saveEdit(id) { + await fetch(`/api/drafts/${id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: editText }), + }); + setEditing(null); + load(tab); + } + + const pendingCount = tab === 'pending' ? total : 0; + + return ( +
+
+
+

+ Черновики +

+

+ {tab === 'pending' && total > 0 + ? `${total} ${total === 1 ? 'пост ждёт' : 'поста ждут'} одобрения` + : 'Авто-генерированные и пакетные посты на проверку'} +

+
+ +
+ + {/* Табы */} +
+ {STATUS_TABS.map(t => ( + + ))} +
+ + {loading &&
} + + {!loading && drafts.length === 0 && ( +
+ +
+ {tab === 'pending' + ? <>Нет черновиков. Включите авто-генерацию в настройках канала или сгенерируйте вручную. + : 'Нет записей'} +
+
+ )} + +
+ {drafts.map(draft => ( +
+ {/* Header */} +
+
+ {draft.channel_name} + {draft.platform && {draft.platform}} +
+
+ + {timeAgo(draft.created_at)} +
+
+ + {/* Тема */} + {draft.topic && ( +
+ 💡 {draft.topic} +
+ )} + + {/* Текст */} + {editing === draft.id ? ( +
+