Добавлены новые зависимости для работы с графиками и статистикой, включая @radix-ui/react-popover, date-fns и react-day-picker. Обновлены компоненты для отображения статистики продаж, улучшена агрегация данных и добавлены функции сортировки в таблицах. Обновлены API маршруты для получения данных о статистике Wildberries. Оптимизирован код для повышения читаемости и производительности.

This commit is contained in:
Bivekich
2025-07-22 14:47:44 +03:00
parent a62a09faca
commit 20c4b665a1
15 changed files with 1688 additions and 486 deletions

View File

@ -1,147 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
const s3Client = new S3Client({
region: 'ru-1',
endpoint: 'https://s3.twcstorage.ru',
credentials: {
accessKeyId: 'I6XD2OR7YO2ZN6L6Z629',
secretAccessKey: '9xCOoafisG0aB9lJNvdLO1UuK73fBvMcpHMdijrJ'
},
forcePathStyle: true
})
const BUCKET_NAME = '617774af-sfera'
// Разрешенные типы изображений
const ALLOWED_IMAGE_TYPES = [
'image/jpeg',
'image/jpg',
'image/png',
'image/webp',
'image/gif'
]
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const file = formData.get('file') as File
const documentType = formData.get('documentType') as string // 'passport' | 'other'
if (!file) {
return NextResponse.json(
{ error: 'File is required' },
{ status: 400 }
)
}
if (!documentType) {
return NextResponse.json(
{ error: 'Document type is required' },
{ status: 400 }
)
}
// Проверяем, что файл не пустой
if (file.size === 0) {
return NextResponse.json(
{ error: 'File is empty' },
{ status: 400 }
)
}
// Проверяем имя файла
if (!file.name || file.name.trim().length === 0) {
return NextResponse.json(
{ error: 'Invalid file name' },
{ status: 400 }
)
}
// Проверяем тип файла - только изображения
if (!ALLOWED_IMAGE_TYPES.includes(file.type)) {
return NextResponse.json(
{ error: `File type ${file.type} is not allowed. Only images are supported.` },
{ status: 400 }
)
}
// Ограничиваем размер файла - 10MB для изображений
const maxSize = 10 * 1024 * 1024
if (file.size > maxSize) {
return NextResponse.json(
{ error: `File size must be less than 10MB` },
{ status: 400 }
)
}
// Генерируем уникальное имя файла
const timestamp = Date.now()
const safeFileName = file.name
.replace(/[^\w\s.-]/g, '_')
.replace(/\s+/g, '_')
.replace(/_{2,}/g, '_')
.toLowerCase()
// Определяем папку в зависимости от типа документа
const folder = `employee-documents/${documentType}`
const key = `${folder}/${timestamp}-${safeFileName}`
// Конвертируем файл в Buffer
const buffer = Buffer.from(await file.arrayBuffer())
// Подготавливаем метаданные
const cleanOriginalName = file.name.replace(/[^\w\s.-]/g, '_')
const metadata = {
originalname: cleanOriginalName,
documenttype: documentType,
uploadtype: 'employee-document'
}
// Загружаем в S3
const command = new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: key,
Body: buffer,
ContentType: file.type,
ACL: 'public-read',
Metadata: metadata
})
await s3Client.send(command)
// Возвращаем URL файла и метаданные
const url = `https://s3.twcstorage.ru/${BUCKET_NAME}/${key}`
return NextResponse.json({
success: true,
url,
key,
originalName: file.name,
size: file.size,
type: file.type,
documentType
})
} catch (error) {
console.error('Error uploading employee document:', error)
let errorMessage = 'Failed to upload document'
if (error instanceof Error) {
if (error.message.includes('Invalid character in header')) {
errorMessage = 'Invalid characters in file name or metadata'
} else if (error.message.includes('AccessDenied')) {
errorMessage = 'Access denied to storage'
} else if (error.message.includes('NoSuchBucket')) {
errorMessage = 'Storage bucket not found'
} else {
errorMessage = error.message
}
}
return NextResponse.json(
{ error: errorMessage, success: false },
{ status: 500 }
)
}
return NextResponse.json({ message: 'Upload employee document API' })
}