- Import real data from PDF (35 Bambu Lab filaments) - Remove all Confluence integration and dependencies - Implement new V2 data structure with proper inventory tracking - Add backwards compatibility for existing data - Create enhanced UI components (ColorSwatch, InventoryBadge, MaterialBadge) - Add advanced filtering with quick filters and multi-criteria search - Organize codebase for dev/prod environments - Update Lambda functions to support both V1/V2 formats - Add inventory summary dashboard - Clean up project structure and documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
343 lines
9.7 KiB
JavaScript
Executable File
343 lines
9.7 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
require('dotenv').config({ path: '.env.local' });
|
|
const AWS = require('aws-sdk');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
|
|
// Configure AWS
|
|
AWS.config.update({
|
|
region: process.env.AWS_REGION || 'eu-central-1'
|
|
});
|
|
|
|
const dynamodb = new AWS.DynamoDB.DocumentClient();
|
|
const TABLE_NAME = process.env.DYNAMODB_TABLE_NAME || 'filamenteka-filaments';
|
|
|
|
// Color mappings for common filament colors
|
|
const colorMappings = {
|
|
'Black': '#000000',
|
|
'White': '#FFFFFF',
|
|
'Red': '#FF0000',
|
|
'Blue': '#0000FF',
|
|
'Green': '#00FF00',
|
|
'Yellow': '#FFFF00',
|
|
'Orange': '#FFA500',
|
|
'Purple': '#800080',
|
|
'Gray': '#808080',
|
|
'Grey': '#808080',
|
|
'Silver': '#C0C0C0',
|
|
'Gold': '#FFD700',
|
|
'Brown': '#964B00',
|
|
'Pink': '#FFC0CB',
|
|
'Cyan': '#00FFFF',
|
|
'Magenta': '#FF00FF',
|
|
'Beige': '#F5F5DC',
|
|
'Transparent': '#FFFFFF00',
|
|
// Specific Bambu Lab colors
|
|
'Mistletoe Green': '#50C878',
|
|
'Indingo Purple': '#4B0082',
|
|
'Jade White': '#F0F8FF',
|
|
'Hot Pink': '#FF69B4',
|
|
'Cocoa Brown': '#D2691E',
|
|
'Cotton Candy Cloud': '#FFB6C1',
|
|
'Sunflower Yellow': '#FFDA03',
|
|
'Scarlet Red': '#FF2400',
|
|
'Mandarin Orange': '#FF8C00',
|
|
'Marine Blue': '#0066CC',
|
|
'Charcoal': '#36454F',
|
|
'Ivory White': '#FFFFF0',
|
|
'Ash Gray': '#B2BEB5',
|
|
'Cobalt Blue': '#0047AB',
|
|
'Turquoise': '#40E0D0',
|
|
'Nardo Gray': '#4A4A4A',
|
|
'Bright Green': '#66FF00',
|
|
'Glow Green': '#90EE90',
|
|
'Black Walnut': '#5C4033',
|
|
'Jeans Blue': '#5670A1',
|
|
'Forest Green': '#228B22',
|
|
'Lavender Purple': '#B57EDC'
|
|
};
|
|
|
|
function getColorHex(colorName) {
|
|
// Try exact match first
|
|
if (colorMappings[colorName]) {
|
|
return colorMappings[colorName];
|
|
}
|
|
|
|
// Try to find color in the name
|
|
for (const [key, value] of Object.entries(colorMappings)) {
|
|
if (colorName.toLowerCase().includes(key.toLowerCase())) {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function parseInventory(oldFilament) {
|
|
let total = 1;
|
|
let vacuum = 0;
|
|
let opened = 0;
|
|
|
|
// Parse kolicina (quantity)
|
|
if (oldFilament.kolicina) {
|
|
const qty = parseInt(oldFilament.kolicina);
|
|
if (!isNaN(qty)) {
|
|
total = qty;
|
|
}
|
|
}
|
|
|
|
// Parse vakum field
|
|
if (oldFilament.vakum) {
|
|
const vakumLower = oldFilament.vakum.toLowerCase();
|
|
if (vakumLower.includes('vakuum') || vakumLower.includes('vakum')) {
|
|
// Check for multiplier
|
|
const match = vakumLower.match(/x(\d+)/);
|
|
vacuum = match ? parseInt(match[1]) : 1;
|
|
}
|
|
}
|
|
|
|
// Parse otvoreno field
|
|
if (oldFilament.otvoreno) {
|
|
const otvorenoLower = oldFilament.otvoreno.toLowerCase();
|
|
if (otvorenoLower.includes('otvorena') || otvorenoLower.includes('otvoreno')) {
|
|
// Check for multiplier
|
|
const match = otvorenoLower.match(/(\d+)x/);
|
|
if (match) {
|
|
opened = parseInt(match[1]);
|
|
} else {
|
|
const match2 = otvorenoLower.match(/x(\d+)/);
|
|
opened = match2 ? parseInt(match2[1]) : 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate available
|
|
const available = vacuum + opened;
|
|
const inUse = Math.max(0, total - available);
|
|
|
|
return {
|
|
total: total || 1,
|
|
available: available,
|
|
inUse: inUse,
|
|
locations: {
|
|
vacuum: vacuum,
|
|
opened: opened,
|
|
printer: 0
|
|
}
|
|
};
|
|
}
|
|
|
|
function determineStorageCondition(oldFilament) {
|
|
if (oldFilament.vakum && oldFilament.vakum.toLowerCase().includes('vakuum')) {
|
|
return 'vacuum';
|
|
}
|
|
if (oldFilament.otvoreno && oldFilament.otvoreno.toLowerCase().includes('otvorena')) {
|
|
return 'opened';
|
|
}
|
|
return 'sealed';
|
|
}
|
|
|
|
function parseMaterial(oldFilament) {
|
|
const base = oldFilament.tip || 'PLA';
|
|
let modifier = null;
|
|
|
|
if (oldFilament.finish && oldFilament.finish !== 'Basic' && oldFilament.finish !== '') {
|
|
modifier = oldFilament.finish;
|
|
}
|
|
|
|
// Handle special PLA types
|
|
if (base === 'PLA' && modifier) {
|
|
// These are actually base materials, not modifiers
|
|
if (modifier === 'PETG' || modifier === 'ABS' || modifier === 'TPU') {
|
|
return { base: modifier, modifier: null };
|
|
}
|
|
}
|
|
|
|
return { base, modifier };
|
|
}
|
|
|
|
function generateSKU(brand, material, color) {
|
|
const brandCode = brand.substring(0, 3).toUpperCase();
|
|
const materialCode = material.base.substring(0, 3);
|
|
const colorCode = color.name.substring(0, 3).toUpperCase();
|
|
const random = Math.random().toString(36).substring(2, 5).toUpperCase();
|
|
return `${brandCode}-${materialCode}-${colorCode}-${random}`;
|
|
}
|
|
|
|
function migrateFilament(oldFilament) {
|
|
const material = parseMaterial(oldFilament);
|
|
const inventory = parseInventory(oldFilament);
|
|
const colorHex = getColorHex(oldFilament.boja);
|
|
|
|
const newFilament = {
|
|
// Keep existing fields
|
|
id: oldFilament.id || uuidv4(),
|
|
sku: generateSKU(oldFilament.brand, material, { name: oldFilament.boja }),
|
|
|
|
// Product info
|
|
brand: oldFilament.brand,
|
|
type: oldFilament.tip || 'PLA',
|
|
material: material,
|
|
color: {
|
|
name: oldFilament.boja || 'Unknown',
|
|
hex: colorHex
|
|
},
|
|
|
|
// Physical properties
|
|
weight: {
|
|
value: 1000, // Default to 1kg
|
|
unit: 'g'
|
|
},
|
|
diameter: 1.75, // Standard diameter
|
|
|
|
// Inventory
|
|
inventory: inventory,
|
|
|
|
// Pricing
|
|
pricing: {
|
|
purchasePrice: oldFilament.cena ? parseFloat(oldFilament.cena) : null,
|
|
currency: 'RSD',
|
|
supplier: null,
|
|
purchaseDate: null
|
|
},
|
|
|
|
// Condition
|
|
condition: {
|
|
isRefill: oldFilament.refill === 'Da',
|
|
openedDate: oldFilament.otvoreno ? new Date().toISOString() : null,
|
|
expiryDate: null,
|
|
storageCondition: determineStorageCondition(oldFilament),
|
|
humidity: null
|
|
},
|
|
|
|
// Metadata
|
|
tags: [],
|
|
notes: null,
|
|
images: [],
|
|
|
|
// Timestamps
|
|
createdAt: oldFilament.createdAt || new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
lastUsed: null,
|
|
|
|
// Keep old structure temporarily for backwards compatibility
|
|
_legacy: {
|
|
tip: oldFilament.tip,
|
|
finish: oldFilament.finish,
|
|
boja: oldFilament.boja,
|
|
refill: oldFilament.refill,
|
|
vakum: oldFilament.vakum,
|
|
otvoreno: oldFilament.otvoreno,
|
|
kolicina: oldFilament.kolicina,
|
|
cena: oldFilament.cena,
|
|
status: oldFilament.status
|
|
}
|
|
};
|
|
|
|
// Add tags based on properties
|
|
if (material.modifier === 'Silk') newFilament.tags.push('silk');
|
|
if (material.modifier === 'Matte') newFilament.tags.push('matte');
|
|
if (material.modifier === 'CF') newFilament.tags.push('engineering', 'carbon-fiber');
|
|
if (material.modifier === 'Wood') newFilament.tags.push('specialty', 'wood-fill');
|
|
if (material.modifier === 'Glow') newFilament.tags.push('specialty', 'glow-in-dark');
|
|
if (material.base === 'PETG') newFilament.tags.push('engineering', 'chemical-resistant');
|
|
if (material.base === 'ABS') newFilament.tags.push('engineering', 'high-temp');
|
|
if (material.base === 'TPU') newFilament.tags.push('flexible', 'engineering');
|
|
if (newFilament.condition.isRefill) newFilament.tags.push('refill', 'eco-friendly');
|
|
|
|
return newFilament;
|
|
}
|
|
|
|
async function migrateData() {
|
|
console.log('Starting migration to new data structure...');
|
|
|
|
try {
|
|
// Scan all existing items
|
|
const scanParams = {
|
|
TableName: TABLE_NAME
|
|
};
|
|
|
|
const items = [];
|
|
let lastEvaluatedKey = null;
|
|
|
|
do {
|
|
if (lastEvaluatedKey) {
|
|
scanParams.ExclusiveStartKey = lastEvaluatedKey;
|
|
}
|
|
|
|
const result = await dynamodb.scan(scanParams).promise();
|
|
items.push(...result.Items);
|
|
lastEvaluatedKey = result.LastEvaluatedKey;
|
|
} while (lastEvaluatedKey);
|
|
|
|
console.log(`Found ${items.length} items to migrate`);
|
|
|
|
// Check if already migrated
|
|
if (items.length > 0 && items[0].material && items[0].inventory) {
|
|
console.log('Data appears to be already migrated!');
|
|
const confirm = process.argv.includes('--force');
|
|
if (!confirm) {
|
|
console.log('Use --force flag to force migration');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Migrate each item
|
|
const migratedItems = items.map(item => migrateFilament(item));
|
|
|
|
// Show sample
|
|
console.log('\nSample migrated data:');
|
|
console.log(JSON.stringify(migratedItems[0], null, 2));
|
|
|
|
// Update items in batches
|
|
const chunks = [];
|
|
for (let i = 0; i < migratedItems.length; i += 25) {
|
|
chunks.push(migratedItems.slice(i, i + 25));
|
|
}
|
|
|
|
console.log(`\nUpdating ${migratedItems.length} items in DynamoDB...`);
|
|
|
|
for (const chunk of chunks) {
|
|
const params = {
|
|
RequestItems: {
|
|
[TABLE_NAME]: chunk.map(item => ({
|
|
PutRequest: { Item: item }
|
|
}))
|
|
}
|
|
};
|
|
|
|
await dynamodb.batchWrite(params).promise();
|
|
console.log(`Updated ${chunk.length} items`);
|
|
}
|
|
|
|
console.log('\n✅ Migration completed successfully!');
|
|
|
|
// Show summary
|
|
const summary = {
|
|
totalItems: migratedItems.length,
|
|
brands: [...new Set(migratedItems.map(i => i.brand))],
|
|
materials: [...new Set(migratedItems.map(i => i.material.base))],
|
|
modifiers: [...new Set(migratedItems.map(i => i.material.modifier).filter(Boolean))],
|
|
storageConditions: [...new Set(migratedItems.map(i => i.condition.storageCondition))],
|
|
totalInventory: migratedItems.reduce((sum, i) => sum + i.inventory.total, 0),
|
|
availableInventory: migratedItems.reduce((sum, i) => sum + i.inventory.available, 0)
|
|
};
|
|
|
|
console.log('\nMigration Summary:');
|
|
console.log(JSON.stringify(summary, null, 2));
|
|
|
|
} catch (error) {
|
|
console.error('Migration failed:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run migration
|
|
if (require.main === module) {
|
|
console.log('Data Structure Migration Tool');
|
|
console.log('============================');
|
|
console.log('This will migrate all filaments to the new structure');
|
|
console.log('Old data will be preserved in _legacy field\n');
|
|
|
|
migrateData();
|
|
} |