+ Черновики
+
+ + {tab === 'pending' && total > 0 + ? `${total} ${total === 1 ? 'пост ждёт' : 'поста ждут'} одобрения` + : 'Авто-генерированные и пакетные посты на проверку'} +
+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 ? 'пост ждёт' : 'поста ждут'} одобрения`
+ : 'Авто-генерированные и пакетные посты на проверку'}
+
+
+
+ Система генерирует посты каждый день — ты одобряешь вечером +
++ Черновики появляются на странице{' '} + Черновики. + Там можно редактировать, одобрять и планировать публикацию. +
+