267 lines
8.3 KiB
TypeScript
267 lines
8.3 KiB
TypeScript
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<string, { message: string }> };
|
||
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 }
|
||
);
|
||
}
|
||
}
|