import { NextRequest, NextResponse } from 'next/server'; import { adminApiDisabledResponse, isAdminApiEnabled } from '@/lib/runtime/adminApi'; import type { FunnelDefinition } from '@/lib/funnel/types'; interface RouteParams { params: Promise<{ id: string; }>; } // No normalization needed: we require `progressbars` for loaders // GET /api/funnels/[id] - получить конкретную воронку export async function GET(request: NextRequest, { params }: RouteParams) { if (!isAdminApiEnabled()) { return adminApiDisabledResponse(); } try { const { id } = await params; const [{ default: connectMongoDB }, { default: FunnelModel }] = await Promise.all([ import('@/lib/mongodb'), import('@/lib/models/Funnel'), ]); await connectMongoDB(); const funnel = await FunnelModel.findById(id); if (!funnel) { return NextResponse.json( { error: 'Funnel not found' }, { status: 404 } ); } return NextResponse.json({ _id: funnel._id, name: funnel.name, description: funnel.description, status: funnel.status, version: funnel.version, createdAt: funnel.createdAt, updatedAt: funnel.updatedAt, publishedAt: funnel.publishedAt, usage: funnel.usage, funnelData: funnel.funnelData }); } catch (error) { console.error('GET /api/funnels/[id] error:', error); return NextResponse.json( { error: 'Failed to fetch funnel' }, { status: 500 } ); } } // PUT /api/funnels/[id] - обновить воронку export async function PUT(request: NextRequest, { params }: RouteParams) { if (!isAdminApiEnabled()) { return adminApiDisabledResponse(); } try { const { id } = await params; const [ { default: connectMongoDB }, { default: FunnelModel }, { default: FunnelHistoryModel }, ] = await Promise.all([ import('@/lib/mongodb'), import('@/lib/models/Funnel'), import('@/lib/models/FunnelHistory'), ]); await connectMongoDB(); const body = await request.json(); const { name, description, funnelData, status, sessionId, actionDescription } = body; // Валидация if (funnelData && (!funnelData.meta || !funnelData.meta.id || !Array.isArray(funnelData.screens))) { return NextResponse.json( { error: 'Invalid funnel data structure' }, { status: 400 } ); } const funnel = await FunnelModel.findById(id); if (!funnel) { return NextResponse.json( { error: 'Funnel not found' }, { status: 404 } ); } // Сохраняем предыдущее состояние для истории const previousData = funnel.funnelData; // Обновляем поля if (name !== undefined) funnel.name = name; if (description !== undefined) funnel.description = description; // Логика версионирования: // - При сохранении (без смены статуса) - версия НЕ увеличивается // - При публикации - версия увеличивается и меняется статус const isPublishing = status === 'published' && funnel.status !== 'published'; if (status !== undefined) funnel.status = status; if (funnelData !== undefined) { // Save as-is; schema expects `progressbars` for loaders funnel.funnelData = funnelData as FunnelDefinition; // Увеличиваем версию только при публикации if (isPublishing) { funnel.version += 1; funnel.publishedAt = new Date(); } } funnel.lastModifiedBy = 'current-user'; // TODO: заменить на реального пользователя const savedFunnel = await funnel.save(); // Создаем запись в истории, если обновлялась структура воронки if (funnelData && sessionId) { // Получаем текущий номер последовательности const lastHistoryEntry = await FunnelHistoryModel .findOne({ funnelId: id, sessionId }) .sort({ sequenceNumber: -1 }); const nextSequenceNumber = (lastHistoryEntry?.sequenceNumber || -1) + 1; await FunnelHistoryModel.create({ funnelId: id, sessionId, funnelSnapshot: funnelData as FunnelDefinition, actionType: status === 'published' ? 'publish' : 'update', sequenceNumber: nextSequenceNumber, description: actionDescription || 'Воронка обновлена', isBaseline: status === 'published', changeDetails: { action: 'update-funnel', previousValue: previousData, newValue: funnelData as FunnelDefinition } }); // Очищаем старые записи истории (оставляем 100) const KEEP_ENTRIES = 100; const entriesToDelete = await FunnelHistoryModel .find({ funnelId: id, sessionId }) .sort({ sequenceNumber: -1 }) .skip(KEEP_ENTRIES) .select('_id'); if (entriesToDelete.length > 0) { const idsToDelete = entriesToDelete.map((entry: { _id: unknown }) => entry._id); await FunnelHistoryModel.deleteMany({ _id: { $in: idsToDelete } }); } } return NextResponse.json({ _id: savedFunnel._id, name: savedFunnel.name, description: savedFunnel.description, status: savedFunnel.status, version: savedFunnel.version, createdAt: savedFunnel.createdAt, updatedAt: savedFunnel.updatedAt, publishedAt: savedFunnel.publishedAt, usage: savedFunnel.usage, funnelData: savedFunnel.funnelData }); } catch (error) { console.error('PUT /api/funnels/[id] error:', error); // Обработка ошибок валидации Mongoose if (error instanceof Error && error.name === 'ValidationError') { const validationError = error as Error & { errors: Record }; const details = []; // Собираем все ошибки валидации for (const field in validationError.errors) { const fieldError = validationError.errors[field]; details.push(`${field}: ${fieldError.message}`); } return NextResponse.json( { error: 'Ошибка валидации данных воронки', details: details.join('; ') }, { status: 400 } ); } // Обработка других ошибок const errorMessage = error instanceof Error ? error.message : 'Неизвестная ошибка'; return NextResponse.json( { error: 'Ошибка сохранения воронки', details: errorMessage }, { status: 500 } ); } } // DELETE /api/funnels/[id] - удалить воронку export async function DELETE(request: NextRequest, { params }: RouteParams) { if (!isAdminApiEnabled()) { return adminApiDisabledResponse(); } try { const { id } = await params; const [ { default: connectMongoDB }, { default: FunnelModel }, { default: FunnelHistoryModel }, ] = await Promise.all([ import('@/lib/mongodb'), import('@/lib/models/Funnel'), import('@/lib/models/FunnelHistory'), ]); await connectMongoDB(); const funnel = await FunnelModel.findById(id); if (!funnel) { return NextResponse.json( { error: 'Funnel not found' }, { status: 404 } ); } // Проверяем статус - опубликованные воронки нельзя удалять напрямую if (funnel.status === 'published') { return NextResponse.json( { error: 'Cannot delete published funnel. Archive it first.' }, { status: 400 } ); } // Удаляем воронку и всю связанную историю await Promise.all([ FunnelModel.findByIdAndDelete(id), FunnelHistoryModel.deleteMany({ funnelId: id }) ]); return NextResponse.json({ message: 'Funnel deleted successfully' }); } catch (error) { console.error('DELETE /api/funnels/[id] error:', error); return NextResponse.json( { error: 'Failed to delete funnel' }, { status: 500 } ); } }