Major restructure: Remove Confluence, add V2 data structure, organize for dev/prod
- 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>
This commit is contained in:
BIN
lambda/auth.zip
BIN
lambda/auth.zip
Binary file not shown.
@@ -9,8 +9,9 @@ const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': process.env.CORS_ORIGIN || '*',
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Authorization',
|
||||
'Access-Control-Allow-Methods': 'POST,OPTIONS'
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token',
|
||||
'Access-Control-Allow-Methods': 'POST,OPTIONS',
|
||||
'Access-Control-Allow-Credentials': 'true'
|
||||
};
|
||||
|
||||
// Helper function to create response
|
||||
|
||||
Binary file not shown.
@@ -8,8 +8,9 @@ const TABLE_NAME = process.env.TABLE_NAME;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': process.env.CORS_ORIGIN || '*',
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Authorization',
|
||||
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS'
|
||||
'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token',
|
||||
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
|
||||
'Access-Control-Allow-Credentials': 'true'
|
||||
};
|
||||
|
||||
// Helper function to create response
|
||||
@@ -19,6 +20,30 @@ const createResponse = (statusCode, body) => ({
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
// Helper to transform new structure to legacy format for backwards compatibility
|
||||
const transformToLegacy = (item) => {
|
||||
if (item._legacy) {
|
||||
// New structure - extract legacy fields
|
||||
return {
|
||||
id: item.id,
|
||||
brand: item.brand,
|
||||
tip: item._legacy.tip || item.type || item.material?.base,
|
||||
finish: item._legacy.finish || item.material?.modifier || 'Basic',
|
||||
boja: item._legacy.boja || item.color?.name,
|
||||
refill: item._legacy.refill || (item.condition?.isRefill ? 'Da' : ''),
|
||||
vakum: item._legacy.vakum || (item.condition?.storageCondition === 'vacuum' ? 'vakuum' : ''),
|
||||
otvoreno: item._legacy.otvoreno || (item.condition?.storageCondition === 'opened' ? 'otvorena' : ''),
|
||||
kolicina: item._legacy.kolicina || String(item.inventory?.total || ''),
|
||||
cena: item._legacy.cena || String(item.pricing?.purchasePrice || ''),
|
||||
status: item._legacy.status,
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt
|
||||
};
|
||||
}
|
||||
// Already in legacy format
|
||||
return item;
|
||||
};
|
||||
|
||||
// GET all filaments or filter by query params
|
||||
const getFilaments = async (event) => {
|
||||
try {
|
||||
@@ -28,45 +53,82 @@ const getFilaments = async (event) => {
|
||||
TableName: TABLE_NAME
|
||||
};
|
||||
|
||||
// If filtering by brand, type, or status, use the appropriate index
|
||||
if (queryParams.brand) {
|
||||
// Support both old and new query parameters
|
||||
const brand = queryParams.brand;
|
||||
const materialBase = queryParams.material || queryParams.tip;
|
||||
const storageCondition = queryParams.storageCondition || queryParams.status;
|
||||
|
||||
// Filter by brand
|
||||
if (brand) {
|
||||
params = {
|
||||
...params,
|
||||
IndexName: 'brand-index',
|
||||
KeyConditionExpression: 'brand = :brand',
|
||||
ExpressionAttributeValues: {
|
||||
':brand': queryParams.brand
|
||||
':brand': brand
|
||||
}
|
||||
};
|
||||
const result = await dynamodb.query(params).promise();
|
||||
return createResponse(200, result.Items);
|
||||
} else if (queryParams.tip) {
|
||||
}
|
||||
// Filter by material (supports old 'tip' param)
|
||||
else if (materialBase) {
|
||||
// Try new index first, fall back to old
|
||||
try {
|
||||
params = {
|
||||
...params,
|
||||
IndexName: 'material-base-index',
|
||||
KeyConditionExpression: '#mb = :mb',
|
||||
ExpressionAttributeNames: {
|
||||
'#mb': 'material.base'
|
||||
},
|
||||
ExpressionAttributeValues: {
|
||||
':mb': materialBase
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
// Fall back to old index
|
||||
params = {
|
||||
...params,
|
||||
IndexName: 'tip-index',
|
||||
KeyConditionExpression: 'tip = :tip',
|
||||
ExpressionAttributeValues: {
|
||||
':tip': materialBase
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
// Filter by storage condition
|
||||
else if (storageCondition) {
|
||||
params = {
|
||||
...params,
|
||||
IndexName: 'tip-index',
|
||||
KeyConditionExpression: 'tip = :tip',
|
||||
FilterExpression: '#sc = :sc',
|
||||
ExpressionAttributeNames: {
|
||||
'#sc': 'condition.storageCondition'
|
||||
},
|
||||
ExpressionAttributeValues: {
|
||||
':tip': queryParams.tip
|
||||
':sc': storageCondition
|
||||
}
|
||||
};
|
||||
const result = await dynamodb.query(params).promise();
|
||||
return createResponse(200, result.Items);
|
||||
} else if (queryParams.status) {
|
||||
params = {
|
||||
...params,
|
||||
IndexName: 'status-index',
|
||||
KeyConditionExpression: 'status = :status',
|
||||
ExpressionAttributeValues: {
|
||||
':status': queryParams.status
|
||||
}
|
||||
};
|
||||
const result = await dynamodb.query(params).promise();
|
||||
return createResponse(200, result.Items);
|
||||
}
|
||||
|
||||
// Get all items
|
||||
const result = await dynamodb.scan(params).promise();
|
||||
return createResponse(200, result.Items);
|
||||
// Execute query or scan
|
||||
let result;
|
||||
if (params.IndexName || params.KeyConditionExpression) {
|
||||
result = await dynamodb.query(params).promise();
|
||||
} else {
|
||||
result = await dynamodb.scan(params).promise();
|
||||
}
|
||||
|
||||
// Check if client supports new format
|
||||
const acceptsNewFormat = event.headers?.['X-Accept-Format'] === 'v2';
|
||||
|
||||
if (acceptsNewFormat) {
|
||||
// Return new format
|
||||
return createResponse(200, result.Items);
|
||||
} else {
|
||||
// Transform to legacy format for backwards compatibility
|
||||
const legacyItems = result.Items.map(transformToLegacy);
|
||||
return createResponse(200, legacyItems);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting filaments:', error);
|
||||
return createResponse(500, { error: 'Failed to fetch filaments' });
|
||||
@@ -96,27 +158,92 @@ const getFilament = async (event) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to generate SKU
|
||||
const generateSKU = (brand, material, color) => {
|
||||
const brandCode = brand.substring(0, 3).toUpperCase();
|
||||
const materialCode = material.substring(0, 3).toUpperCase();
|
||||
const colorCode = color.substring(0, 3).toUpperCase();
|
||||
const random = Math.random().toString(36).substring(2, 5).toUpperCase();
|
||||
return `${brandCode}-${materialCode}-${colorCode}-${random}`;
|
||||
};
|
||||
|
||||
// POST - Create new filament
|
||||
const createFilament = async (event) => {
|
||||
try {
|
||||
const body = JSON.parse(event.body);
|
||||
const timestamp = new Date().toISOString();
|
||||
const acceptsNewFormat = event.headers?.['X-Accept-Format'] === 'v2';
|
||||
|
||||
// Determine status based on vakum and otvoreno fields
|
||||
let status = 'new';
|
||||
if (body.otvoreno && body.otvoreno.toLowerCase().includes('otvorena')) {
|
||||
status = 'opened';
|
||||
} else if (body.refill && body.refill.toLowerCase() === 'da') {
|
||||
status = 'refill';
|
||||
let item;
|
||||
|
||||
if (acceptsNewFormat || body.material) {
|
||||
// New format
|
||||
item = {
|
||||
id: uuidv4(),
|
||||
sku: body.sku || generateSKU(body.brand, body.material.base, body.color.name),
|
||||
...body,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp
|
||||
};
|
||||
} else {
|
||||
// Legacy format - convert to new structure
|
||||
const material = {
|
||||
base: body.tip || 'PLA',
|
||||
modifier: body.finish !== 'Basic' ? body.finish : null
|
||||
};
|
||||
|
||||
const storageCondition = body.vakum?.toLowerCase().includes('vakuum') ? 'vacuum' :
|
||||
body.otvoreno?.toLowerCase().includes('otvorena') ? 'opened' : 'sealed';
|
||||
|
||||
item = {
|
||||
id: uuidv4(),
|
||||
sku: generateSKU(body.brand, material.base, body.boja),
|
||||
brand: body.brand,
|
||||
type: body.tip || 'PLA',
|
||||
material: material,
|
||||
color: {
|
||||
name: body.boja,
|
||||
hex: null
|
||||
},
|
||||
weight: {
|
||||
value: 1000,
|
||||
unit: 'g'
|
||||
},
|
||||
diameter: 1.75,
|
||||
inventory: {
|
||||
total: parseInt(body.kolicina) || 1,
|
||||
available: storageCondition === 'opened' ? 0 : 1,
|
||||
inUse: 0,
|
||||
locations: {
|
||||
vacuum: storageCondition === 'vacuum' ? 1 : 0,
|
||||
opened: storageCondition === 'opened' ? 1 : 0,
|
||||
printer: 0
|
||||
}
|
||||
},
|
||||
pricing: {
|
||||
purchasePrice: body.cena ? parseFloat(body.cena) : null,
|
||||
currency: 'RSD'
|
||||
},
|
||||
condition: {
|
||||
isRefill: body.refill === 'Da',
|
||||
storageCondition: storageCondition
|
||||
},
|
||||
tags: [],
|
||||
_legacy: {
|
||||
tip: body.tip,
|
||||
finish: body.finish,
|
||||
boja: body.boja,
|
||||
refill: body.refill,
|
||||
vakum: body.vakum,
|
||||
otvoreno: body.otvoreno,
|
||||
kolicina: body.kolicina,
|
||||
cena: body.cena,
|
||||
status: body.status || 'new'
|
||||
},
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp
|
||||
};
|
||||
}
|
||||
|
||||
const item = {
|
||||
id: uuidv4(),
|
||||
...body,
|
||||
status,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp
|
||||
};
|
||||
|
||||
const params = {
|
||||
TableName: TABLE_NAME,
|
||||
@@ -124,7 +251,10 @@ const createFilament = async (event) => {
|
||||
};
|
||||
|
||||
await dynamodb.put(params).promise();
|
||||
return createResponse(201, item);
|
||||
|
||||
// Return in appropriate format
|
||||
const response = acceptsNewFormat ? item : transformToLegacy(item);
|
||||
return createResponse(201, response);
|
||||
} catch (error) {
|
||||
console.error('Error creating filament:', error);
|
||||
return createResponse(500, { error: 'Failed to create filament' });
|
||||
|
||||
Reference in New Issue
Block a user