feat: модульная архитектура sidebar и улучшения навигации
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
377
scripts/migrate-v1-to-v2-inventory.ts
Normal file
377
scripts/migrate-v1-to-v2-inventory.ts
Normal file
@ -0,0 +1,377 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* СКРИПТ МИГРАЦИИ V1 → V2 INVENTORY SYSTEM
|
||||
*
|
||||
* Безопасно переносит данные из старой системы Supply в новую FulfillmentConsumableInventory
|
||||
* с возможностью отката и детальной отчетностью
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
interface MigrationStats {
|
||||
totalV1Records: number
|
||||
fulfillmentSupplies: number
|
||||
migratedRecords: number
|
||||
skippedRecords: number
|
||||
errors: string[]
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
interface V1SupplyRecord {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
price: any // Decimal
|
||||
quantity: number
|
||||
unit: string
|
||||
category: string | null
|
||||
status: string
|
||||
date: Date
|
||||
supplier: string
|
||||
minStock: number
|
||||
currentStock: number
|
||||
organizationId: string
|
||||
type: string | null
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
const MIGRATION_LOG_PREFIX = '[V1→V2 MIGRATION]'
|
||||
|
||||
/**
|
||||
* Главная функция миграции
|
||||
*/
|
||||
async function migrateV1ToV2Inventory(dryRun = true): Promise<MigrationStats> {
|
||||
const stats: MigrationStats = {
|
||||
totalV1Records: 0,
|
||||
fulfillmentSupplies: 0,
|
||||
migratedRecords: 0,
|
||||
skippedRecords: 0,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
}
|
||||
|
||||
console.log(`${MIGRATION_LOG_PREFIX} Starting migration (DRY RUN: ${dryRun})`)
|
||||
console.log(`${MIGRATION_LOG_PREFIX} Timestamp: ${new Date().toISOString()}`)
|
||||
|
||||
try {
|
||||
// Получаем все записи из старой системы
|
||||
const v1Supplies = await prisma.supply.findMany({
|
||||
include: {
|
||||
organization: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
})
|
||||
|
||||
stats.totalV1Records = v1Supplies.length
|
||||
console.log(`${MIGRATION_LOG_PREFIX} Found ${stats.totalV1Records} V1 Supply records`)
|
||||
|
||||
// Фильтруем только расходники фулфилмента
|
||||
const fulfillmentSupplies = v1Supplies.filter(supply =>
|
||||
supply.organization?.type === 'FULFILLMENT' &&
|
||||
supply.type === 'FULFILLMENT_CONSUMABLES'
|
||||
)
|
||||
|
||||
stats.fulfillmentSupplies = fulfillmentSupplies.length
|
||||
console.log(`${MIGRATION_LOG_PREFIX} Found ${stats.fulfillmentSupplies} Fulfillment consumables to migrate`)
|
||||
|
||||
if (stats.fulfillmentSupplies === 0) {
|
||||
console.log(`${MIGRATION_LOG_PREFIX} No fulfillment supplies to migrate`)
|
||||
return stats
|
||||
}
|
||||
|
||||
// Создаем соответствующие Product записи для каждого Supply (если еще нет)
|
||||
const productMap = new Map<string, string>()
|
||||
|
||||
for (const supply of fulfillmentSupplies) {
|
||||
try {
|
||||
const productId = await ensureProductExists(supply, dryRun)
|
||||
productMap.set(supply.id, productId)
|
||||
} catch (error) {
|
||||
const errorMsg = `Failed to create/find product for supply ${supply.id}: ${error}`
|
||||
stats.errors.push(errorMsg)
|
||||
console.error(`${MIGRATION_LOG_PREFIX} ERROR: ${errorMsg}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`${MIGRATION_LOG_PREFIX} Created/verified ${productMap.size} products`)
|
||||
|
||||
// Мигрируем каждую запись Supply → FulfillmentConsumableInventory
|
||||
for (const supply of fulfillmentSupplies) {
|
||||
try {
|
||||
const productId = productMap.get(supply.id)
|
||||
if (!productId) {
|
||||
stats.skippedRecords++
|
||||
continue
|
||||
}
|
||||
|
||||
await migrateSupplyRecord(supply, productId, dryRun)
|
||||
stats.migratedRecords++
|
||||
|
||||
// Логируем прогресс каждые 10 записей
|
||||
if (stats.migratedRecords % 10 === 0) {
|
||||
console.log(`${MIGRATION_LOG_PREFIX} Progress: ${stats.migratedRecords}/${fulfillmentSupplies.length}`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMsg = `Failed to migrate supply ${supply.id}: ${error}`
|
||||
stats.errors.push(errorMsg)
|
||||
console.error(`${MIGRATION_LOG_PREFIX} ERROR: ${errorMsg}`)
|
||||
stats.skippedRecords++
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMsg = `Migration failed: ${error}`
|
||||
stats.errors.push(errorMsg)
|
||||
console.error(`${MIGRATION_LOG_PREFIX} CRITICAL ERROR: ${errorMsg}`)
|
||||
}
|
||||
|
||||
// Финальная отчетность
|
||||
printMigrationReport(stats, dryRun)
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает Product запись для Supply, если еще не существует
|
||||
*/
|
||||
async function ensureProductExists(supply: V1SupplyRecord, dryRun: boolean): Promise<string> {
|
||||
// Проверяем есть ли уже Product с таким названием и организацией
|
||||
const existingProduct = await prisma.product.findFirst({
|
||||
where: {
|
||||
organizationId: supply.organizationId,
|
||||
name: supply.name,
|
||||
type: 'CONSUMABLE',
|
||||
},
|
||||
})
|
||||
|
||||
if (existingProduct) {
|
||||
return existingProduct.id
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`${MIGRATION_LOG_PREFIX} [DRY RUN] Would create product: ${supply.name}`)
|
||||
return `mock-product-id-${supply.id}`
|
||||
}
|
||||
|
||||
// Создаем новый Product на основе Supply данных
|
||||
const newProduct = await prisma.product.create({
|
||||
data: {
|
||||
name: supply.name,
|
||||
article: `MIGRATED-${supply.id.slice(-8)}`, // Уникальный артикул
|
||||
description: supply.description || `Мигрировано из V1 Supply ${supply.id}`,
|
||||
price: supply.price || 0,
|
||||
unit: supply.unit || 'шт',
|
||||
category: supply.category || 'Расходники',
|
||||
type: 'CONSUMABLE',
|
||||
organizationId: supply.organizationId,
|
||||
quantity: 0, // В Product quantity означает что-то другое
|
||||
imageUrl: null,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`${MIGRATION_LOG_PREFIX} Created product: ${newProduct.name} (${newProduct.id})`)
|
||||
return newProduct.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Мигрирует одну запись Supply в FulfillmentConsumableInventory
|
||||
*/
|
||||
async function migrateSupplyRecord(supply: V1SupplyRecord, productId: string, dryRun: boolean): Promise<void> {
|
||||
if (dryRun) {
|
||||
console.log(`${MIGRATION_LOG_PREFIX} [DRY RUN] Would migrate: ${supply.name} (${supply.currentStock} units)`)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем не мигрировали ли уже эту запись
|
||||
const existingInventory = await prisma.fulfillmentConsumableInventory.findUnique({
|
||||
where: {
|
||||
fulfillmentCenterId_productId: {
|
||||
fulfillmentCenterId: supply.organizationId,
|
||||
productId: productId,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (existingInventory) {
|
||||
console.log(`${MIGRATION_LOG_PREFIX} WARNING: Inventory already exists for ${supply.name}, skipping`)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем запись в новой системе инвентаря
|
||||
const inventory = await prisma.fulfillmentConsumableInventory.create({
|
||||
data: {
|
||||
fulfillmentCenterId: supply.organizationId,
|
||||
productId: productId,
|
||||
|
||||
// Складские данные из V1
|
||||
currentStock: supply.currentStock,
|
||||
minStock: supply.minStock,
|
||||
maxStock: null, // В V1 не было
|
||||
reservedStock: 0, // В V1 не было
|
||||
totalReceived: supply.quantity, // Приблизительно
|
||||
totalShipped: Math.max(0, supply.quantity - supply.currentStock), // Приблизительно
|
||||
|
||||
// Цены
|
||||
averageCost: parseFloat(supply.price?.toString() || '0'),
|
||||
resalePrice: null, // В V1 не было
|
||||
|
||||
// Метаданные
|
||||
lastSupplyDate: supply.date,
|
||||
lastUsageDate: null, // В V1 не было точной информации
|
||||
notes: `Мигрировано из V1 Supply ${supply.id} (${supply.status})`,
|
||||
|
||||
// Временные метки сохраняем из V1
|
||||
createdAt: supply.createdAt,
|
||||
updatedAt: supply.updatedAt,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`${MIGRATION_LOG_PREFIX} ✅ Migrated: ${supply.name} → ${inventory.id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Печатает детальный отчет о миграции
|
||||
*/
|
||||
function printMigrationReport(stats: MigrationStats, dryRun: boolean): void {
|
||||
console.log('\n' + '='.repeat(60))
|
||||
console.log(`${MIGRATION_LOG_PREFIX} MIGRATION REPORT`)
|
||||
console.log('='.repeat(60))
|
||||
console.log(`Mode: ${dryRun ? 'DRY RUN' : 'PRODUCTION'}`)
|
||||
console.log(`Timestamp: ${new Date().toISOString()}`)
|
||||
console.log('')
|
||||
console.log('📊 STATISTICS:')
|
||||
console.log(` Total V1 records found: ${stats.totalV1Records}`)
|
||||
console.log(` Fulfillment supplies: ${stats.fulfillmentSupplies}`)
|
||||
console.log(` Successfully migrated: ${stats.migratedRecords}`)
|
||||
console.log(` Skipped records: ${stats.skippedRecords}`)
|
||||
console.log(` Errors encountered: ${stats.errors.length}`)
|
||||
console.log('')
|
||||
|
||||
if (stats.errors.length > 0) {
|
||||
console.log('❌ ERRORS:')
|
||||
stats.errors.forEach((error, index) => {
|
||||
console.log(` ${index + 1}. ${error}`)
|
||||
})
|
||||
console.log('')
|
||||
}
|
||||
|
||||
if (stats.warnings.length > 0) {
|
||||
console.log('⚠️ WARNINGS:')
|
||||
stats.warnings.forEach((warning, index) => {
|
||||
console.log(` ${index + 1}. ${warning}`)
|
||||
})
|
||||
console.log('')
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log('🔄 TO RUN ACTUAL MIGRATION:')
|
||||
console.log(' npm run migrate:v1-to-v2 --production')
|
||||
} else {
|
||||
console.log('✅ MIGRATION COMPLETED!')
|
||||
console.log('🔄 TO VERIFY RESULTS:')
|
||||
console.log(' Check FulfillmentConsumableInventory table')
|
||||
console.log(' Verify counts match expectations')
|
||||
}
|
||||
|
||||
console.log('='.repeat(60))
|
||||
}
|
||||
|
||||
/**
|
||||
* Функция отката миграции (осторожно!)
|
||||
*/
|
||||
async function rollbackMigration(): Promise<void> {
|
||||
console.log(`${MIGRATION_LOG_PREFIX} ⚠️ STARTING ROLLBACK`)
|
||||
|
||||
// Удаляем все мигрированные записи
|
||||
const deleted = await prisma.fulfillmentConsumableInventory.deleteMany({
|
||||
where: {
|
||||
notes: {
|
||||
contains: 'Мигрировано из V1 Supply'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`${MIGRATION_LOG_PREFIX} 🗑️ Deleted ${deleted.count} migrated inventory records`)
|
||||
|
||||
// Удаляем мигрированные Products (осторожно!)
|
||||
const deletedProducts = await prisma.product.deleteMany({
|
||||
where: {
|
||||
article: {
|
||||
startsWith: 'MIGRATED-'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`${MIGRATION_LOG_PREFIX} 🗑️ Deleted ${deletedProducts.count} migrated product records`)
|
||||
console.log(`${MIGRATION_LOG_PREFIX} ✅ Rollback completed`)
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI интерфейс
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2)
|
||||
const isProduction = args.includes('--production')
|
||||
const isRollback = args.includes('--rollback')
|
||||
|
||||
if (isRollback) {
|
||||
if (!isProduction) {
|
||||
console.log('❌ Rollback requires --production flag for safety')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('⚠️ You are about to rollback the V1→V2 migration!')
|
||||
console.log('This will DELETE all migrated data!')
|
||||
console.log('Press Ctrl+C to cancel...')
|
||||
|
||||
// Ждем 5 секунд
|
||||
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||
|
||||
await rollbackMigration()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const dryRun = !isProduction
|
||||
|
||||
if (dryRun) {
|
||||
console.log('🔍 Running in DRY RUN mode (no actual changes)')
|
||||
console.log('📝 Add --production flag to run actual migration')
|
||||
} else {
|
||||
console.log('⚠️ Running in PRODUCTION mode (will make changes)')
|
||||
console.log('Press Ctrl+C to cancel...')
|
||||
|
||||
// Ждем 3 секунды в production режиме
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await migrateV1ToV2Inventory(dryRun)
|
||||
|
||||
if (stats.errors.length > 0) {
|
||||
console.error(`${MIGRATION_LOG_PREFIX} Migration completed with errors`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`${MIGRATION_LOG_PREFIX} Migration completed successfully`)
|
||||
process.exit(0)
|
||||
|
||||
} catch (error) {
|
||||
console.error(`${MIGRATION_LOG_PREFIX} Migration failed:`, error)
|
||||
process.exit(1)
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// Запускаем только если скрипт вызван напрямую (ES modules)
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch(console.error)
|
||||
}
|
||||
|
||||
export { migrateV1ToV2Inventory, rollbackMigration }
|
Reference in New Issue
Block a user