diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..ca8a0fb --- /dev/null +++ b/.env.development @@ -0,0 +1,13 @@ +# Development Environment Configuration +NODE_ENV=development + +# API Configuration +NEXT_PUBLIC_API_URL=https://5wmfjjzm0i.execute-api.eu-central-1.amazonaws.com/dev + +# AWS Configuration +AWS_REGION=eu-central-1 +DYNAMODB_TABLE_NAME=filamenteka-filaments-dev + +# Admin credentials (development) +# Username: admin +# Password: admin123 \ No newline at end of file diff --git a/.env.production b/.env.production index d95aac8..3f6a2e3 100644 --- a/.env.production +++ b/.env.production @@ -1,3 +1,11 @@ -# This file is for Amplify to know which env vars to expose to Next.js -# The actual values come from Amplify Environment Variables -NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} \ No newline at end of file +# Production Environment Configuration +NODE_ENV=production + +# API Configuration +NEXT_PUBLIC_API_URL=https://5wmfjjzm0i.execute-api.eu-central-1.amazonaws.com/production + +# AWS Configuration +AWS_REGION=eu-central-1 +DYNAMODB_TABLE_NAME=filamenteka-filaments + +# Admin credentials are stored in AWS Secrets Manager in production \ No newline at end of file diff --git a/.gitignore b/.gitignore index f0fe422..98dc31b 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,8 @@ terraform/*.tfplan terraform/override.tf terraform/override.tf.json terraform/*_override.tf -terraform/*_override.tf.json \ No newline at end of file +terraform/*_override.tf.json + +# Lambda packages +lambda/*.zip +lambda/**/node_modules/ \ No newline at end of file diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..8ac5666 --- /dev/null +++ b/PROJECT_STRUCTURE.md @@ -0,0 +1,121 @@ +# Project Structure + +## Overview +Filamenteka is organized with clear separation between environments, infrastructure, and application code. + +## Directory Structure + +``` +filamenteka/ +├── app/ # Next.js app directory +│ ├── page.tsx # Main page +│ ├── layout.tsx # Root layout +│ ├── admin/ # Admin pages +│ │ ├── page.tsx # Admin login +│ │ └── dashboard/ # Admin dashboard +│ └── globals.css # Global styles +│ +├── src/ # Source code +│ ├── components/ # React components +│ │ ├── FilamentTable.tsx +│ │ ├── FilamentForm.tsx +│ │ └── ColorCell.tsx +│ ├── types/ # TypeScript types +│ │ └── filament.ts +│ ├── data/ # Data and utilities +│ │ └── bambuLabColors.ts +│ └── styles/ # Component styles +│ └── select.css +│ +├── lambda/ # AWS Lambda functions +│ ├── filaments/ # Filaments CRUD API +│ │ ├── index.js +│ │ └── package.json +│ └── auth/ # Authentication API +│ ├── index.js +│ └── package.json +│ +├── terraform/ # Infrastructure as Code +│ ├── environments/ # Environment-specific configs +│ │ ├── dev/ +│ │ └── prod/ +│ ├── main.tf # Main Terraform configuration +│ ├── dynamodb.tf # DynamoDB tables +│ ├── lambda.tf # Lambda functions +│ ├── api_gateway.tf # API Gateway +│ └── variables.tf # Variable definitions +│ +├── scripts/ # Utility scripts +│ ├── data-import/ # Data import tools +│ │ ├── import-pdf-data.js +│ │ └── clear-dynamo.js +│ ├── security/ # Security checks +│ │ └── security-check.js +│ └── build/ # Build scripts +│ +├── config/ # Configuration files +│ └── environments.js # Environment configuration +│ +└── public/ # Static assets +``` + +## Environment Files + +- `.env.development` - Development environment variables +- `.env.production` - Production environment variables +- `.env.local` - Local overrides (not committed) +- `.env.development.local` - Local dev overrides (not committed) + +## Key Concepts + +### Environments +- **Development**: Uses `dev` API Gateway stage and separate DynamoDB table +- **Production**: Uses `production` API Gateway stage and main DynamoDB table + +### Data Flow +1. Frontend (Next.js) → API Gateway → Lambda Functions → DynamoDB +2. Authentication via JWT tokens stored in localStorage +3. Real-time data updates every 5 minutes + +### Infrastructure +- Managed via Terraform +- Separate resources for dev/prod +- AWS services: DynamoDB, Lambda, API Gateway, Amplify + +## Development Workflow + +1. **Local Development** + ```bash + npm run dev + ``` + +2. **Deploy to Dev** + ```bash + cd terraform + terraform apply -var-file=environments/dev/terraform.tfvars + ``` + +3. **Deploy to Production** + ```bash + cd terraform + terraform apply -var-file=environments/prod/terraform.tfvars + ``` + +## Data Management + +### Import Data from PDF +```bash +node scripts/data-import/import-pdf-data.js +``` + +### Clear DynamoDB Table +```bash +node scripts/data-import/clear-dynamo.js +``` + +## Security + +- No hardcoded credentials +- JWT authentication for admin +- Environment-specific configurations +- Pre-commit security checks \ No newline at end of file diff --git a/README.md b/README.md index 01cf085..c7a3ec5 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # Filamenteka -A web application for tracking Bambu Lab filament inventory with automatic color coding, synced from Confluence documentation. +A web application for tracking Bambu Lab filament inventory with automatic color coding. ## Features - 🎨 **Automatic Color Coding** - Table rows are automatically colored based on filament colors -- 🔄 **Confluence Sync** - Pulls filament data from Confluence table every 5 minutes - 🔍 **Search & Filter** - Quick search across all filament properties - 📊 **Sortable Columns** - Click headers to sort by any column - 🌈 **Gradient Support** - Special handling for gradient filaments like Cotton Candy Cloud @@ -14,7 +13,7 @@ A web application for tracking Bambu Lab filament inventory with automatic color ## Technology Stack - **Frontend**: React + TypeScript + Tailwind CSS -- **Backend**: API routes for Confluence integration +- **Backend**: Next.js API routes - **Infrastructure**: AWS Amplify (Frankfurt region) - **IaC**: Terraform @@ -24,7 +23,6 @@ A web application for tracking Bambu Lab filament inventory with automatic color - AWS Account - Terraform 1.0+ - GitHub account -- Confluence account with API access ## Setup Instructions @@ -41,14 +39,7 @@ cd filamenteka npm install ``` -### 3. Configure Confluence Access - -Create a Confluence API token: -1. Go to https://id.atlassian.com/manage-profile/security/api-tokens -2. Create a new API token -3. Note your Confluence domain and the page ID containing your filament table - -### 4. Deploy with Terraform +### 3. Deploy with Terraform ```bash cd terraform @@ -60,23 +51,9 @@ terraform plan terraform apply ``` -### 5. Environment Variables - -The following environment variables are needed: -- `CONFLUENCE_API_URL` - Your Confluence instance URL (e.g., https://your-domain.atlassian.net) -- `CONFLUENCE_TOKEN` - Your Confluence API token -- `CONFLUENCE_PAGE_ID` - The ID of the Confluence page containing the filament table - ## Local Development ```bash -# Create .env file for local development -cat > .env << EOF -CONFLUENCE_API_URL=https://your-domain.atlassian.net -CONFLUENCE_TOKEN=your_api_token -CONFLUENCE_PAGE_ID=your_page_id -EOF - # Run development server npm run dev ``` @@ -85,7 +62,7 @@ Visit http://localhost:5173 to see the app. ## Table Format -Your Confluence table should have these columns: +The filament table should have these columns: - **Brand** - Manufacturer (e.g., BambuLab) - **Tip** - Material type (e.g., PLA, PETG, ABS) - **Finish** - Finish type (e.g., Basic, Matte, Silk) @@ -131,13 +108,8 @@ export const bambuLabColors: Record = { ## Troubleshooting -### Confluence Connection Issues -- Verify your API token is valid -- Check the page ID is correct -- Ensure your Confluence user has read access to the page - ### Color Not Showing -- Check if the color name in Confluence matches exactly +- Check if the color name matches exactly - Add the color mapping to `bambuLabColors.ts` - Colors are case-insensitive but spelling must match diff --git a/__tests__/no-mock-data.test.ts b/__tests__/no-mock-data.test.ts new file mode 100644 index 0000000..fd10d8b --- /dev/null +++ b/__tests__/no-mock-data.test.ts @@ -0,0 +1,29 @@ +import { readFileSync, existsSync } from 'fs'; +import { join } from 'path'; + +describe('No Mock Data Tests', () => { + it('should not have data.json in public folder', () => { + const dataJsonPath = join(process.cwd(), 'public', 'data.json'); + expect(existsSync(dataJsonPath)).toBe(false); + }); + + it('should not have fallback to data.json in page.tsx', () => { + const pagePath = join(process.cwd(), 'app', 'page.tsx'); + const pageContent = readFileSync(pagePath, 'utf-8'); + + expect(pageContent).not.toContain('data.json'); + expect(pageContent).not.toContain("'/data.json'"); + expect(pageContent).toContain('API URL not configured'); + }); + + it('should use NEXT_PUBLIC_API_URL in all components', () => { + const pagePath = join(process.cwd(), 'app', 'page.tsx'); + const adminPath = join(process.cwd(), 'app', 'admin', 'dashboard', 'page.tsx'); + + const pageContent = readFileSync(pagePath, 'utf-8'); + const adminContent = readFileSync(adminPath, 'utf-8'); + + expect(pageContent).toContain('process.env.NEXT_PUBLIC_API_URL'); + expect(adminContent).toContain('process.env.NEXT_PUBLIC_API_URL'); + }); +}); \ No newline at end of file diff --git a/__tests__/security.test.ts b/__tests__/security.test.ts deleted file mode 100644 index 571b358..0000000 --- a/__tests__/security.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Mock Next.js server components -jest.mock('next/server', () => ({ - NextResponse: { - json: (data: any, init?: ResponseInit) => ({ - json: async () => data, - ...init - }) - } -})); - -// Mock confluence module -jest.mock('../src/server/confluence', () => ({ - fetchFromConfluence: jest.fn() -})); - -import { GET } from '../app/api/filaments/route'; -import { fetchFromConfluence } from '../src/server/confluence'; - -describe('API Security Tests', () => { - const originalEnv = process.env; - - beforeEach(() => { - jest.resetModules(); - process.env = { ...originalEnv }; - }); - - afterEach(() => { - process.env = originalEnv; - }); - - it('should not expose credentials in error responses', async () => { - // Simulate missing environment variables - delete process.env.CONFLUENCE_TOKEN; - - const response = await GET(); - const data = await response.json(); - - // Check that response doesn't contain sensitive information - expect(JSON.stringify(data)).not.toContain('ATATT'); - expect(JSON.stringify(data)).not.toContain('token'); - expect(JSON.stringify(data)).not.toContain('password'); - expect(data.error).toBe('Server configuration error'); - }); - - it('should not expose internal error details', async () => { - // Set valid environment - process.env.CONFLUENCE_API_URL = 'https://test.atlassian.net'; - process.env.CONFLUENCE_TOKEN = 'test-token'; - process.env.CONFLUENCE_PAGE_ID = 'test-page'; - - // Mock fetchFromConfluence to throw an error - const mockFetchFromConfluence = fetchFromConfluence as jest.MockedFunction; - mockFetchFromConfluence.mockRejectedValueOnce(new Error('Internal database error with sensitive details')); - - const response = await GET(); - const data = await response.json(); - - // Should get generic error, not specific details - expect(data.error).toBe('Failed to fetch filaments'); - expect(data).not.toHaveProperty('stack'); - expect(data).not.toHaveProperty('message'); - expect(JSON.stringify(data)).not.toContain('Internal database error'); - expect(JSON.stringify(data)).not.toContain('sensitive details'); - }); -}); \ No newline at end of file diff --git a/amplify.yml b/amplify.yml index 91617a9..c00a95b 100644 --- a/amplify.yml +++ b/amplify.yml @@ -5,11 +5,8 @@ frontend: commands: - npm ci - npm run security:check - # Print env vars for debugging (without exposing values) - - env | grep CONFLUENCE | sed 's/=.*/=***/' build: commands: - - npx tsx scripts/fetch-data.js - npm run build artifacts: baseDirectory: out diff --git a/app/page.tsx b/app/page.tsx index 94cb1ae..790fa8e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { FilamentTable } from '../src/components/FilamentTable'; +import { FilamentTableV2 } from '../src/components/FilamentTableV2'; import { Filament } from '../src/types/filament'; import axios from 'axios'; @@ -12,6 +13,7 @@ export default function Home() { const [lastUpdate, setLastUpdate] = useState(null); const [darkMode, setDarkMode] = useState(false); const [mounted, setMounted] = useState(false); + const [useV2, setUseV2] = useState(true); // Default to new UI // Initialize dark mode from localStorage after mounting useEffect(() => { @@ -39,17 +41,22 @@ export default function Home() { setLoading(true); setError(null); - // Use API if available, fallback to static JSON const apiUrl = process.env.NEXT_PUBLIC_API_URL; - const url = apiUrl ? `${apiUrl}/filaments` : '/data.json'; - console.log('Fetching from:', url); - console.log('API URL configured:', apiUrl); - const response = await axios.get(url); - console.log('Response data:', response.data); + if (!apiUrl) { + throw new Error('API URL not configured'); + } + const url = `${apiUrl}/filaments`; + const headers = useV2 ? { 'X-Accept-Format': 'v2' } : {}; + const response = await axios.get(url, { headers }); setFilaments(response.data); setLastUpdate(new Date()); } catch (err) { - setError(err instanceof Error ? err.message : 'Greška pri učitavanju filamenata'); + console.error('API Error:', err); + if (axios.isAxiosError(err)) { + setError(`API Error: ${err.response?.status || 'Network'} - ${err.message}`); + } else { + setError(err instanceof Error ? err.message : 'Greška pri učitavanju filamenata'); + } } finally { setLoading(false); } @@ -86,13 +93,22 @@ export default function Home() { {loading ? 'Ažuriranje...' : 'Osveži'} {mounted && ( - + <> + + + )} @@ -100,11 +116,19 @@ export default function Home() {
- + {useV2 ? ( + + ) : ( + + )}
diff --git a/config/environments.js b/config/environments.js new file mode 100644 index 0000000..afc69fc --- /dev/null +++ b/config/environments.js @@ -0,0 +1,23 @@ +// Environment configuration +const environments = { + development: { + name: 'development', + apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api', + dynamoTableName: 'filamenteka-filaments-dev', + awsRegion: 'eu-central-1' + }, + production: { + name: 'production', + apiUrl: process.env.NEXT_PUBLIC_API_URL, + dynamoTableName: 'filamenteka-filaments', + awsRegion: 'eu-central-1' + } +}; + +const currentEnv = process.env.NODE_ENV || 'development'; + +module.exports = { + env: environments[currentEnv] || environments.development, + isDev: currentEnv === 'development', + isProd: currentEnv === 'production' +}; \ No newline at end of file diff --git a/docs/DATA_STRUCTURE_PROPOSAL.md b/docs/DATA_STRUCTURE_PROPOSAL.md new file mode 100644 index 0000000..1256369 --- /dev/null +++ b/docs/DATA_STRUCTURE_PROPOSAL.md @@ -0,0 +1,255 @@ +# Improved Data Structure Proposal + +## Current Issues +1. Mixed languages (English/Serbian) +2. String fields for numeric/boolean values +3. Inconsistent status representation +4. No proper inventory tracking +5. Missing important metadata + +## Proposed Structure + +```typescript +interface Filament { + // Identifiers + id: string; + sku?: string; // For internal tracking + + // Product Info + brand: string; + type: 'PLA' | 'PETG' | 'ABS' | 'TPU' | 'SILK' | 'CF' | 'WOOD'; + material: { + base: 'PLA' | 'PETG' | 'ABS' | 'TPU'; + modifier?: 'Silk' | 'Matte' | 'Glow' | 'Wood' | 'CF'; + }; + color: { + name: string; + hex?: string; // Color code for UI display + pantone?: string; // For color matching + }; + + // Physical Properties + weight: { + value: number; // 1000 for 1kg, 500 for 0.5kg + unit: 'g' | 'kg'; + }; + diameter: number; // 1.75 or 2.85 + + // Inventory Status + inventory: { + total: number; // Total spools + available: number; // Available for use + inUse: number; // Currently being used + locations: { + vacuum: number; // In vacuum storage + opened: number; // Opened but usable + printer: number; // Loaded in printer + }; + }; + + // Purchase Info + pricing: { + purchasePrice?: number; + currency: 'RSD' | 'EUR' | 'USD'; + supplier?: string; + purchaseDate?: string; + }; + + // Condition + condition: { + isRefill: boolean; + openedDate?: string; + expiryDate?: string; + storageCondition: 'vacuum' | 'sealed' | 'opened' | 'desiccant'; + humidity?: number; // Last measured + }; + + // Metadata + tags: string[]; // ['premium', 'engineering', 'easy-print'] + notes?: string; // Special handling instructions + images?: string[]; // S3 URLs for photos + + // Timestamps + createdAt: string; + updatedAt: string; + lastUsed?: string; +} +``` + +## Benefits + +### 1. **Better Filtering** +```typescript +// Find all sealed PLA under 1kg +filaments.filter(f => + f.material.base === 'PLA' && + f.weight.value <= 1000 && + f.condition.storageCondition === 'vacuum' +) +``` + +### 2. **Inventory Management** +```typescript +// Get total available filament weight +const totalWeight = filaments.reduce((sum, f) => + sum + (f.inventory.available * f.weight.value), 0 +); + +// Find low stock items +const lowStock = filaments.filter(f => + f.inventory.available <= 1 && f.inventory.total > 0 +); +``` + +### 3. **Color Management** +```typescript +// Group by color for visualization +const colorGroups = filaments.reduce((groups, f) => { + const color = f.color.name; + groups[color] = groups[color] || []; + groups[color].push(f); + return groups; +}, {}); +``` + +### 4. **Usage Tracking** +```typescript +// Find most used filaments +const mostUsed = filaments + .filter(f => f.lastUsed) + .sort((a, b) => new Date(b.lastUsed) - new Date(a.lastUsed)) + .slice(0, 10); +``` + +## Migration Strategy + +### Phase 1: Add New Fields (Non-breaking) +```javascript +// Update Lambda to handle both old and new structure +const migrateFilament = (old) => ({ + ...old, + material: { + base: old.tip || 'PLA', + modifier: old.finish !== 'Basic' ? old.finish : undefined + }, + color: { + name: old.boja + }, + weight: { + value: 1000, // Default 1kg + unit: 'g' + }, + inventory: { + total: parseInt(old.kolicina) || 1, + available: old.otvoreno ? 0 : 1, + inUse: 0, + locations: { + vacuum: old.vakum ? 1 : 0, + opened: old.otvoreno ? 1 : 0, + printer: 0 + } + }, + condition: { + isRefill: old.refill === 'Da', + storageCondition: old.vakum ? 'vacuum' : (old.otvoreno ? 'opened' : 'sealed') + } +}); +``` + +### Phase 2: Update UI Components +- Create new filter components for material type +- Add inventory status indicators +- Color preview badges +- Storage condition icons + +### Phase 3: Enhanced Features +1. **Barcode/QR Integration**: Generate QR codes for each spool +2. **Usage History**: Track which prints used which filament +3. **Alerts**: Low stock, expiry warnings +4. **Analytics**: Cost per print, filament usage trends + +## DynamoDB Optimization + +### Current Indexes +- brand-index +- tip-index +- status-index + +### Proposed Indexes +```terraform +global_secondary_index { + name = "material-color-index" + hash_key = "material.base" + range_key = "color.name" +} + +global_secondary_index { + name = "inventory-status-index" + hash_key = "condition.storageCondition" + range_key = "inventory.available" +} + +global_secondary_index { + name = "brand-type-index" + hash_key = "brand" + range_key = "material.base" +} +``` + +## Example Queries + +### Find all available green filaments +```javascript +const greenFilaments = await dynamodb.query({ + IndexName: 'material-color-index', + FilterExpression: 'contains(color.name, :green) AND inventory.available > :zero', + ExpressionAttributeValues: { + ':green': 'Green', + ':zero': 0 + } +}).promise(); +``` + +### Get inventory summary +```javascript +const summary = await dynamodb.scan({ + TableName: TABLE_NAME, + ProjectionExpression: 'brand, material.base, inventory' +}).promise(); + +const report = summary.Items.reduce((acc, item) => { + const key = `${item.brand}-${item.material.base}`; + acc[key] = (acc[key] || 0) + item.inventory.total; + return acc; +}, {}); +``` + +## UI Improvements + +### 1. **Visual Inventory Status** +```tsx +
+ {filament.inventory.locations.vacuum > 0 && ( + + )} + {filament.inventory.locations.opened > 0 && ( + + )} +
+``` + +### 2. **Color Swatches** +```tsx +
+``` + +### 3. **Smart Filters** +- Quick filters: "Ready to use", "Low stock", "Refills only" +- Material groups: "Standard PLA", "Engineering", "Specialty" +- Storage status: "Vacuum sealed", "Open spools", "In printer" + +Would you like me to implement this improved structure? \ No newline at end of file diff --git a/lambda/auth.zip b/lambda/auth.zip deleted file mode 100644 index 5f033bc..0000000 Binary files a/lambda/auth.zip and /dev/null differ diff --git a/lambda/auth/index.js b/lambda/auth/index.js index 9e957ea..10d84d1 100644 --- a/lambda/auth/index.js +++ b/lambda/auth/index.js @@ -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 diff --git a/lambda/filaments.zip b/lambda/filaments.zip deleted file mode 100644 index 5a99612..0000000 Binary files a/lambda/filaments.zip and /dev/null differ diff --git a/lambda/filaments/index.js b/lambda/filaments/index.js index f1f8bca..462c095 100644 --- a/lambda/filaments/index.js +++ b/lambda/filaments/index.js @@ -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' }); diff --git a/package.json b/package.json index 7b03987..2bfb6cc 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "next lint", "test": "jest", "test:watch": "jest --watch", - "security:check": "node scripts/security-check.js", + "security:check": "node scripts/security/security-check.js", "test:build": "node scripts/test-build.js", "prepare": "husky", "migrate": "cd scripts && npm install && npm run migrate", diff --git a/public/data.json b/public/data.json deleted file mode 100644 index 63fe1be..0000000 --- a/public/data.json +++ /dev/null @@ -1,167 +0,0 @@ -[ - { - "brand": "Bambu Lab", - "tip": "PLA", - "finish": "Basic", - "boja": "Lavender Purple", - "refill": "", - "vakum": "u vakuumu", - "otvoreno": "", - "kolicina": "1kg", - "cena": "2500" - }, - { - "brand": "Bambu Lab", - "tip": "PLA", - "finish": "Matte", - "boja": "Charcoal Black", - "refill": "", - "vakum": "", - "otvoreno": "otvorena", - "kolicina": "0.8kg", - "cena": "2800" - }, - { - "brand": "Bambu Lab", - "tip": "PETG", - "finish": "Basic", - "boja": "Transparent", - "refill": "Da", - "vakum": "u vakuumu", - "otvoreno": "", - "kolicina": "1kg", - "cena": "3200" - }, - { - "brand": "Azure Film", - "tip": "PLA", - "finish": "Basic", - "boja": "White", - "refill": "", - "vakum": "u vakuumu", - "otvoreno": "", - "kolicina": "1kg", - "cena": "2200" - }, - { - "brand": "Azure Film", - "tip": "PETG", - "finish": "Basic", - "boja": "Orange", - "refill": "", - "vakum": "", - "otvoreno": "otvorena", - "kolicina": "0.5kg", - "cena": "2600" - }, - { - "brand": "Bambu Lab", - "tip": "Silk PLA", - "finish": "Silk", - "boja": "Gold", - "refill": "Da", - "vakum": "", - "otvoreno": "", - "kolicina": "0.5kg", - "cena": "3500" - }, - { - "brand": "Bambu Lab", - "tip": "PLA Matte", - "finish": "Matte", - "boja": "Forest Green", - "refill": "", - "vakum": "u vakuumu", - "otvoreno": "", - "kolicina": "1kg", - "cena": "2800" - }, - { - "brand": "PanaChroma", - "tip": "PLA", - "finish": "Basic", - "boja": "Red", - "refill": "", - "vakum": "u vakuumu", - "otvoreno": "", - "kolicina": "1kg", - "cena": "2300" - }, - { - "brand": "PanaChroma", - "tip": "PETG", - "finish": "Basic", - "boja": "Blue", - "refill": "", - "vakum": "", - "otvoreno": "otvorena", - "kolicina": "0.75kg", - "cena": "2700" - }, - { - "brand": "Fiberlogy", - "tip": "PLA", - "finish": "Basic", - "boja": "Gray", - "refill": "", - "vakum": "u vakuumu", - "otvoreno": "", - "kolicina": "0.85kg", - "cena": "2400" - }, - { - "brand": "Fiberlogy", - "tip": "ABS", - "finish": "Basic", - "boja": "Black", - "refill": "", - "vakum": "", - "otvoreno": "", - "kolicina": "1kg", - "cena": "2900" - }, - { - "brand": "Fiberlogy", - "tip": "TPU", - "finish": "Basic", - "boja": "Lime Green", - "refill": "", - "vakum": "", - "otvoreno": "otvorena", - "kolicina": "0.3kg", - "cena": "4500" - }, - { - "brand": "Azure Film", - "tip": "PLA", - "finish": "Silk", - "boja": "Silver", - "refill": "Da", - "vakum": "u vakuumu", - "otvoreno": "", - "kolicina": "1kg", - "cena": "2800" - }, - { - "brand": "Bambu Lab", - "tip": "PLA", - "finish": "Basic", - "boja": "Jade White", - "refill": "", - "vakum": "", - "otvoreno": "otvorena", - "kolicina": "0.5kg", - "cena": "2500" - }, - { - "brand": "PanaChroma", - "tip": "Silk PLA", - "finish": "Silk", - "boja": "Copper", - "refill": "", - "vakum": "u vakuumu", - "otvoreno": "", - "kolicina": "1kg", - "cena": "3200" - } -] \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index a6b0681..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# Data Migration Scripts - -This directory contains scripts for migrating filament data from Confluence to DynamoDB. - -## Prerequisites - -1. AWS credentials configured (either via AWS CLI or environment variables) -2. DynamoDB table created via Terraform -3. Confluence API credentials (if migrating from Confluence) - -## Setup - -```bash -cd scripts -npm install -``` - -## Configuration - -Create a `.env.local` file in the project root with: - -```env -# AWS Configuration -AWS_REGION=eu-central-1 -DYNAMODB_TABLE_NAME=filamenteka-filaments - -# Confluence Configuration (optional) -CONFLUENCE_API_URL=https://your-domain.atlassian.net -CONFLUENCE_TOKEN=your-email:your-api-token -CONFLUENCE_PAGE_ID=your-page-id -``` - -## Usage - -### Migrate from local data (data.json) - -```bash -npm run migrate -``` - -### Clear existing data and migrate - -```bash -npm run migrate:clear -``` - -### Manual execution - -```bash -# Migrate without clearing -node migrate-with-parser.js - -# Clear existing data first -node migrate-with-parser.js --clear -``` - -## What the script does - -1. **Checks for Confluence credentials** - - If found: Fetches data from Confluence page - - If not found: Uses local `public/data.json` file - -2. **Parses the data** - - Extracts filament information from HTML table (Confluence) - - Or reads JSON directly (local file) - -3. **Prepares data for DynamoDB** - - Generates unique IDs for each filament - - Adds timestamps (createdAt, updatedAt) - -4. **Writes to DynamoDB** - - Writes in batches of 25 items (DynamoDB limit) - - Shows progress during migration - -5. **Verifies the migration** - - Counts total items in DynamoDB - - Shows a sample item for verification - -## Troubleshooting - -- **Table not found**: Make sure you've run `terraform apply` first -- **Access denied**: Check your AWS credentials and permissions -- **Confluence errors**: Verify your API token and page ID -- **Empty migration**: Check that the Confluence page has a table with the expected format \ No newline at end of file diff --git a/scripts/data-import/clear-dynamo.js b/scripts/data-import/clear-dynamo.js new file mode 100755 index 0000000..5fceee7 --- /dev/null +++ b/scripts/data-import/clear-dynamo.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +require('dotenv').config({ path: '.env.local' }); +const AWS = require('aws-sdk'); + +// 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'; + +async function clearTable() { + console.log(`Clearing all items from ${TABLE_NAME}...`); + + try { + // First, scan to get all items + const scanParams = { + TableName: TABLE_NAME, + ProjectionExpression: 'id' + }; + + 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 delete`); + + // Delete in batches of 25 + const chunks = []; + for (let i = 0; i < items.length; i += 25) { + chunks.push(items.slice(i, i + 25)); + } + + for (const chunk of chunks) { + const params = { + RequestItems: { + [TABLE_NAME]: chunk.map(item => ({ + DeleteRequest: { Key: { id: item.id } } + })) + } + }; + + await dynamodb.batchWrite(params).promise(); + console.log(`Deleted ${chunk.length} items`); + } + + console.log('Table cleared successfully!'); + } catch (error) { + console.error('Error clearing table:', error); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + clearTable(); +} \ No newline at end of file diff --git a/scripts/data-import/import-pdf-data.js b/scripts/data-import/import-pdf-data.js new file mode 100755 index 0000000..d1c209d --- /dev/null +++ b/scripts/data-import/import-pdf-data.js @@ -0,0 +1,179 @@ +#!/usr/bin/env node + +require('dotenv').config({ path: '.env.local' }); +const AWS = require('aws-sdk'); +const { v4: uuidv4 } = require('uuid'); +const fs = require('fs'); +const path = require('path'); + +// 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'; + +async function clearTable() { + console.log(`Clearing all items from ${TABLE_NAME}...`); + + try { + // First, scan to get all items + const scanParams = { + TableName: TABLE_NAME, + ProjectionExpression: 'id' + }; + + 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 delete`); + + if (items.length === 0) { + console.log('Table is already empty'); + return; + } + + // Delete in batches of 25 + const chunks = []; + for (let i = 0; i < items.length; i += 25) { + chunks.push(items.slice(i, i + 25)); + } + + for (const chunk of chunks) { + const params = { + RequestItems: { + [TABLE_NAME]: chunk.map(item => ({ + DeleteRequest: { Key: { id: item.id } } + })) + } + }; + + await dynamodb.batchWrite(params).promise(); + console.log(`Deleted ${chunk.length} items`); + } + + console.log('Table cleared successfully!'); + } catch (error) { + console.error('Error clearing table:', error); + throw error; + } +} + +async function importData() { + console.log('Importing data from PDF...'); + + try { + // Read the PDF data + const pdfData = JSON.parse( + fs.readFileSync(path.join(__dirname, 'pdf-filaments.json'), 'utf8') + ); + + console.log(`Found ${pdfData.length} filaments to import`); + + // Process each filament + const timestamp = new Date().toISOString(); + const processedFilaments = pdfData.map(filament => { + // Determine status based on vakum and otvoreno fields + let status = 'new'; + if (filament.otvoreno && filament.otvoreno.toLowerCase().includes('otvorena')) { + status = 'opened'; + } else if (filament.refill && filament.refill.toLowerCase() === 'da') { + status = 'refill'; + } + + // Clean up finish field - if empty, default to "Basic" + const finish = filament.finish || 'Basic'; + + return { + id: uuidv4(), + ...filament, + finish, + status, + createdAt: timestamp, + updatedAt: timestamp + }; + }); + + // Import to DynamoDB in batches + const chunks = []; + for (let i = 0; i < processedFilaments.length; i += 25) { + chunks.push(processedFilaments.slice(i, i + 25)); + } + + let totalImported = 0; + for (const chunk of chunks) { + const params = { + RequestItems: { + [TABLE_NAME]: chunk.map(item => ({ + PutRequest: { Item: item } + })) + } + }; + + await dynamodb.batchWrite(params).promise(); + totalImported += chunk.length; + console.log(`Imported ${totalImported}/${processedFilaments.length} items`); + } + + console.log('Import completed successfully!'); + + // Verify the import + const scanParams = { + TableName: TABLE_NAME, + Select: 'COUNT' + }; + + const result = await dynamodb.scan(scanParams).promise(); + console.log(`\nVerification: ${result.Count} total items now in DynamoDB`); + + // Show sample data + const sampleParams = { + TableName: TABLE_NAME, + Limit: 3 + }; + + const sampleResult = await dynamodb.scan(sampleParams).promise(); + console.log('\nSample imported data:'); + sampleResult.Items.forEach(item => { + console.log(`- ${item.brand} ${item.tip} ${item.finish} - ${item.boja} (${item.status})`); + }); + + } catch (error) { + console.error('Error importing data:', error); + throw error; + } +} + +async function main() { + try { + console.log('PDF Data Import Tool'); + console.log('==================='); + + // Clear existing data + await clearTable(); + + // Import new data + await importData(); + + console.log('\n✅ Import completed successfully!'); + } catch (error) { + console.error('\n❌ Import failed:', error); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + main(); +} \ No newline at end of file diff --git a/scripts/data-import/migrate-to-new-structure.js b/scripts/data-import/migrate-to-new-structure.js new file mode 100755 index 0000000..f8bf44b --- /dev/null +++ b/scripts/data-import/migrate-to-new-structure.js @@ -0,0 +1,343 @@ +#!/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(); +} \ No newline at end of file diff --git a/scripts/fetch-data.js b/scripts/fetch-data.js deleted file mode 100644 index f558eb6..0000000 --- a/scripts/fetch-data.js +++ /dev/null @@ -1,36 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { fetchFromConfluence } = require('../src/server/confluence.ts'); - -async function fetchData() { - console.log('Fetching data from Confluence...'); - - const env = { - CONFLUENCE_API_URL: process.env.CONFLUENCE_API_URL, - CONFLUENCE_TOKEN: process.env.CONFLUENCE_TOKEN, - CONFLUENCE_PAGE_ID: process.env.CONFLUENCE_PAGE_ID - }; - - try { - const data = await fetchFromConfluence(env); - - // Create public directory if it doesn't exist - const publicDir = path.join(__dirname, '..', 'public'); - if (!fs.existsSync(publicDir)) { - fs.mkdirSync(publicDir, { recursive: true }); - } - - // Write data to public directory - fs.writeFileSync( - path.join(publicDir, 'data.json'), - JSON.stringify(data, null, 2) - ); - - console.log(`✅ Fetched ${data.length} filaments`); - } catch (error) { - console.error('❌ Failed to fetch data:', error.message); - process.exit(1); - } -} - -fetchData(); \ No newline at end of file diff --git a/scripts/migrate-confluence-to-dynamo.js b/scripts/migrate-confluence-to-dynamo.js deleted file mode 100644 index 3b9d2e5..0000000 --- a/scripts/migrate-confluence-to-dynamo.js +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env node - -require('dotenv').config({ path: '.env.local' }); -const axios = require('axios'); -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'; - -// Confluence configuration -const CONFLUENCE_API_URL = process.env.CONFLUENCE_API_URL; -const CONFLUENCE_TOKEN = process.env.CONFLUENCE_TOKEN; -const CONFLUENCE_PAGE_ID = process.env.CONFLUENCE_PAGE_ID; - -async function fetchConfluenceData() { - try { - console.log('Fetching data from Confluence...'); - - const response = await axios.get( - `${CONFLUENCE_API_URL}/wiki/rest/api/content/${CONFLUENCE_PAGE_ID}?expand=body.storage`, - { - headers: { - 'Authorization': `Basic ${Buffer.from(CONFLUENCE_TOKEN).toString('base64')}`, - 'Accept': 'application/json' - } - } - ); - - const htmlContent = response.data.body.storage.value; - return parseConfluenceTable(htmlContent); - } catch (error) { - console.error('Error fetching from Confluence:', error.message); - throw error; - } -} - -function parseConfluenceTable(html) { - // Simple HTML table parser - in production, use a proper HTML parser like cheerio - const rows = []; - const tableRegex = /]*>(.*?)<\/tr>/gs; - const cellRegex = /]*>(.*?)<\/t[dh]>/gs; - - let match; - let isHeader = true; - - while ((match = tableRegex.exec(html)) !== null) { - const rowHtml = match[1]; - const cells = []; - let cellMatch; - - while ((cellMatch = cellRegex.exec(rowHtml)) !== null) { - // Remove HTML tags from cell content - const cellContent = cellMatch[1] - .replace(/<[^>]*>/g, '') - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .trim(); - cells.push(cellContent); - } - - if (!isHeader && cells.length > 0) { - rows.push(cells); - } - isHeader = false; - } - - // Map rows to filament objects - return rows.map(row => ({ - brand: row[0] || '', - tip: row[1] || '', - finish: row[2] || '', - boja: row[3] || '', - refill: row[4] || '', - vakum: row[5] || '', - otvoreno: row[6] || '', - kolicina: row[7] || '', - cena: row[8] || '' - })); -} - -async function migrateToLocalJSON() { - try { - console.log('Migrating to local JSON file for testing...'); - - // For now, use the mock data we created - const fs = require('fs'); - const data = JSON.parse(fs.readFileSync('./public/data.json', 'utf8')); - - const filaments = data.map(item => ({ - id: uuidv4(), - ...item, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - })); - - console.log(`Found ${filaments.length} filaments to migrate`); - return filaments; - } catch (error) { - console.error('Error reading local data:', error); - throw error; - } -} - -async function migrateToDynamoDB(filaments) { - console.log(`Migrating ${filaments.length} filaments to DynamoDB...`); - - // Check if table exists - try { - const dynamo = new AWS.DynamoDB(); - await dynamo.describeTable({ TableName: TABLE_NAME }).promise(); - console.log(`Table ${TABLE_NAME} exists`); - } catch (error) { - if (error.code === 'ResourceNotFoundException') { - console.error(`Table ${TABLE_NAME} does not exist. Please run Terraform first.`); - process.exit(1); - } - throw error; - } - - // Batch write items - const chunks = []; - for (let i = 0; i < filaments.length; i += 25) { - chunks.push(filaments.slice(i, i + 25)); - } - - for (const chunk of chunks) { - const params = { - RequestItems: { - [TABLE_NAME]: chunk.map(item => ({ - PutRequest: { Item: item } - })) - } - }; - - try { - await dynamodb.batchWrite(params).promise(); - console.log(`Migrated ${chunk.length} items`); - } catch (error) { - console.error('Error writing batch:', error); - throw error; - } - } - - console.log('Migration completed successfully!'); -} - -async function main() { - try { - let filaments; - - if (CONFLUENCE_API_URL && CONFLUENCE_TOKEN && CONFLUENCE_PAGE_ID) { - // Fetch from Confluence - const confluenceData = await fetchConfluenceData(); - filaments = confluenceData.map(item => ({ - id: uuidv4(), - ...item, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - })); - } else { - console.log('Confluence credentials not found, using local data...'); - filaments = await migrateToLocalJSON(); - } - - // Migrate to DynamoDB - await migrateToDynamoDB(filaments); - - // Verify migration - const params = { - TableName: TABLE_NAME, - Select: 'COUNT' - }; - - const result = await dynamodb.scan(params).promise(); - console.log(`\nVerification: ${result.Count} items in DynamoDB`); - - } catch (error) { - console.error('Migration failed:', error); - process.exit(1); - } -} - -// Run migration -if (require.main === module) { - main(); -} \ No newline at end of file diff --git a/scripts/migrate-with-parser.js b/scripts/migrate-with-parser.js deleted file mode 100644 index 77281b1..0000000 --- a/scripts/migrate-with-parser.js +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env node - -require('dotenv').config({ path: '.env.local' }); -const axios = require('axios'); -const AWS = require('aws-sdk'); -const { v4: uuidv4 } = require('uuid'); -const cheerio = require('cheerio'); - -// 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'; - -// Confluence configuration -const CONFLUENCE_API_URL = process.env.CONFLUENCE_API_URL; -const CONFLUENCE_TOKEN = process.env.CONFLUENCE_TOKEN; -const CONFLUENCE_PAGE_ID = process.env.CONFLUENCE_PAGE_ID; - -async function fetchConfluenceData() { - try { - console.log('Fetching data from Confluence...'); - - const response = await axios.get( - `${CONFLUENCE_API_URL}/wiki/rest/api/content/${CONFLUENCE_PAGE_ID}?expand=body.storage`, - { - headers: { - 'Authorization': `Basic ${Buffer.from(CONFLUENCE_TOKEN).toString('base64')}`, - 'Accept': 'application/json' - } - } - ); - - const htmlContent = response.data.body.storage.value; - return parseConfluenceTable(htmlContent); - } catch (error) { - console.error('Error fetching from Confluence:', error.message); - throw error; - } -} - -function parseConfluenceTable(html) { - const $ = cheerio.load(html); - const filaments = []; - - // Find the table and iterate through rows - $('table').find('tr').each((index, row) => { - // Skip header row - if (index === 0) return; - - const cells = $(row).find('td'); - if (cells.length >= 9) { - const filament = { - brand: $(cells[0]).text().trim(), - tip: $(cells[1]).text().trim(), - finish: $(cells[2]).text().trim(), - boja: $(cells[3]).text().trim(), - refill: $(cells[4]).text().trim(), - vakum: $(cells[5]).text().trim(), - otvoreno: $(cells[6]).text().trim(), - kolicina: $(cells[7]).text().trim(), - cena: $(cells[8]).text().trim() - }; - - // Only add if row has valid data - if (filament.brand || filament.boja) { - filaments.push(filament); - } - } - }); - - return filaments; -} - -async function clearDynamoTable() { - console.log('Clearing existing data from DynamoDB...'); - - // Scan all items - const scanParams = { - TableName: TABLE_NAME, - ProjectionExpression: 'id' - }; - - try { - const scanResult = await dynamodb.scan(scanParams).promise(); - - if (scanResult.Items.length === 0) { - console.log('Table is already empty'); - return; - } - - // Delete in batches - const deleteRequests = scanResult.Items.map(item => ({ - DeleteRequest: { Key: { id: item.id } } - })); - - // DynamoDB batchWrite supports max 25 items - for (let i = 0; i < deleteRequests.length; i += 25) { - const batch = deleteRequests.slice(i, i + 25); - const params = { - RequestItems: { - [TABLE_NAME]: batch - } - }; - - await dynamodb.batchWrite(params).promise(); - console.log(`Deleted ${batch.length} items`); - } - - console.log('Table cleared successfully'); - } catch (error) { - console.error('Error clearing table:', error); - throw error; - } -} - -async function migrateToDynamoDB(filaments) { - console.log(`Migrating ${filaments.length} filaments to DynamoDB...`); - - // Check if table exists - try { - const dynamo = new AWS.DynamoDB(); - await dynamo.describeTable({ TableName: TABLE_NAME }).promise(); - console.log(`Table ${TABLE_NAME} exists`); - } catch (error) { - if (error.code === 'ResourceNotFoundException') { - console.error(`Table ${TABLE_NAME} does not exist. Please run Terraform first.`); - process.exit(1); - } - throw error; - } - - // Add IDs and timestamps - const itemsToInsert = filaments.map(item => ({ - id: uuidv4(), - ...item, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - })); - - // Batch write items (max 25 per batch) - const chunks = []; - for (let i = 0; i < itemsToInsert.length; i += 25) { - chunks.push(itemsToInsert.slice(i, i + 25)); - } - - let totalMigrated = 0; - for (const chunk of chunks) { - const params = { - RequestItems: { - [TABLE_NAME]: chunk.map(item => ({ - PutRequest: { Item: item } - })) - } - }; - - try { - await dynamodb.batchWrite(params).promise(); - totalMigrated += chunk.length; - console.log(`Migrated ${totalMigrated}/${itemsToInsert.length} items`); - } catch (error) { - console.error('Error writing batch:', error); - throw error; - } - } - - console.log('Migration completed successfully!'); - return totalMigrated; -} - -async function main() { - try { - let filaments; - - // Check for --clear flag - const shouldClear = process.argv.includes('--clear'); - - if (shouldClear) { - await clearDynamoTable(); - } - - if (CONFLUENCE_API_URL && CONFLUENCE_TOKEN && CONFLUENCE_PAGE_ID) { - // Fetch from Confluence - console.log('Using Confluence as data source'); - filaments = await fetchConfluenceData(); - } else { - console.log('Confluence credentials not found, using local mock data...'); - const fs = require('fs'); - const data = JSON.parse(fs.readFileSync('../public/data.json', 'utf8')); - filaments = data; - } - - console.log(`Found ${filaments.length} filaments to migrate`); - - // Show sample data - if (filaments.length > 0) { - console.log('\nSample data:'); - console.log(JSON.stringify(filaments[0], null, 2)); - } - - // Migrate to DynamoDB - const migrated = await migrateToDynamoDB(filaments); - - // Verify migration - const params = { - TableName: TABLE_NAME, - Select: 'COUNT' - }; - - const result = await dynamodb.scan(params).promise(); - console.log(`\nVerification: ${result.Count} total items now in DynamoDB`); - - // Show sample from DynamoDB - const sampleParams = { - TableName: TABLE_NAME, - Limit: 1 - }; - - const sampleResult = await dynamodb.scan(sampleParams).promise(); - if (sampleResult.Items.length > 0) { - console.log('\nSample from DynamoDB:'); - console.log(JSON.stringify(sampleResult.Items[0], null, 2)); - } - - } catch (error) { - console.error('Migration failed:', error); - process.exit(1); - } -} - -// Run migration -if (require.main === module) { - console.log('Confluence to DynamoDB Migration Tool'); - console.log('====================================='); - console.log('Usage: node migrate-with-parser.js [--clear]'); - console.log(' --clear: Clear existing data before migration\n'); - - main(); -} \ No newline at end of file diff --git a/scripts/security-check.js b/scripts/security/security-check.js similarity index 100% rename from scripts/security-check.js rename to scripts/security/security-check.js diff --git a/src/components/ColorSwatch.tsx b/src/components/ColorSwatch.tsx new file mode 100644 index 0000000..ccc4961 --- /dev/null +++ b/src/components/ColorSwatch.tsx @@ -0,0 +1,79 @@ +import React from 'react'; + +interface ColorSwatchProps { + name: string; + hex?: string; + size?: 'sm' | 'md' | 'lg'; + showLabel?: boolean; + className?: string; +} + +export const ColorSwatch: React.FC = ({ + name, + hex, + size = 'md', + showLabel = true, + className = '' +}) => { + const sizeClasses = { + sm: 'w-6 h-6', + md: 'w-8 h-8', + lg: 'w-10 h-10' + }; + + // Default color mappings if hex is not provided + const defaultColors: Record = { + 'Black': '#000000', + 'White': '#FFFFFF', + 'Gray': '#808080', + 'Red': '#FF0000', + 'Blue': '#0000FF', + 'Green': '#00FF00', + 'Yellow': '#FFFF00', + 'Transparent': 'rgba(255, 255, 255, 0.1)' + }; + + const getColorFromName = (colorName: string): string => { + // Check exact match first + if (defaultColors[colorName]) return defaultColors[colorName]; + + // Check if color name contains a known color + const lowerName = colorName.toLowerCase(); + for (const [key, value] of Object.entries(defaultColors)) { + if (lowerName.includes(key.toLowerCase())) { + return value; + } + } + + // Generate a color from the name hash + let hash = 0; + for (let i = 0; i < colorName.length; i++) { + hash = colorName.charCodeAt(i) + ((hash << 5) - hash); + } + const hue = hash % 360; + return `hsl(${hue}, 70%, 50%)`; + }; + + const backgroundColor = hex || getColorFromName(name); + const isLight = backgroundColor.startsWith('#') && + parseInt(backgroundColor.slice(1, 3), 16) > 200 && + parseInt(backgroundColor.slice(3, 5), 16) > 200 && + parseInt(backgroundColor.slice(5, 7), 16) > 200; + + return ( +
+
+ {name.toLowerCase().includes('transparent') && ( +
+ )} +
+ {showLabel && ( + {name} + )} +
+ ); +}; \ No newline at end of file diff --git a/src/components/EnhancedFilters.tsx b/src/components/EnhancedFilters.tsx new file mode 100644 index 0000000..44eb177 --- /dev/null +++ b/src/components/EnhancedFilters.tsx @@ -0,0 +1,204 @@ +import React from 'react'; + +interface EnhancedFiltersProps { + filters: { + brand: string; + material: string; + storageCondition: string; + isRefill: boolean | null; + color: string; + }; + onFilterChange: (filters: any) => void; + uniqueValues: { + brands: string[]; + materials: string[]; + colors: string[]; + }; +} + +export const EnhancedFilters: React.FC = ({ + filters, + onFilterChange, + uniqueValues +}) => { + const quickFilters = [ + { id: 'ready', label: 'Spremno za upotrebu', icon: '✅' }, + { id: 'lowStock', label: 'Malo na stanju', icon: '⚠️' }, + { id: 'refills', label: 'Samo punjenja', icon: '♻️' }, + { id: 'sealed', label: 'Zapakovano', icon: '📦' }, + { id: 'opened', label: 'Otvoreno', icon: '📂' } + ]; + + const handleQuickFilter = (filterId: string) => { + switch (filterId) { + case 'ready': + onFilterChange({ ...filters, storageCondition: 'vacuum' }); + break; + case 'lowStock': + // This would need backend support + onFilterChange({ ...filters }); + break; + case 'refills': + onFilterChange({ ...filters, isRefill: true }); + break; + case 'sealed': + onFilterChange({ ...filters, storageCondition: 'vacuum' }); + break; + case 'opened': + onFilterChange({ ...filters, storageCondition: 'opened' }); + break; + } + }; + + return ( +
+ {/* Quick Filters */} +
+

+ Brzi filteri +

+
+ {quickFilters.map(filter => ( + + ))} +
+
+ + {/* Advanced Filters */} +
+ {/* Brand Filter */} +
+ + +
+ + {/* Material Filter */} +
+ + +
+ + {/* Color Filter */} +
+ + +
+ + {/* Storage Condition */} +
+ + +
+ + {/* Refill Toggle */} +
+ + +
+
+ + {/* Clear Filters */} +
+ +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/FilamentTable.tsx b/src/components/FilamentTable.tsx index 76ca44d..1ad9fe6 100644 --- a/src/components/FilamentTable.tsx +++ b/src/components/FilamentTable.tsx @@ -58,8 +58,8 @@ export const FilamentTable: React.FC = ({ filaments, loading }); filtered.sort((a, b) => { - const aValue = a[sortField]; - const bValue = b[sortField]; + const aValue = a[sortField] || ''; + const bValue = b[sortField] || ''; if (sortOrder === 'asc') { return aValue.localeCompare(bValue); diff --git a/src/components/FilamentTableV2.tsx b/src/components/FilamentTableV2.tsx new file mode 100644 index 0000000..a9d8541 --- /dev/null +++ b/src/components/FilamentTableV2.tsx @@ -0,0 +1,300 @@ +import React, { useState, useMemo } from 'react'; +import { FilamentV2, isFilamentV2 } from '../types/filament.v2'; +import { Filament } from '../types/filament'; +import { ColorSwatch } from './ColorSwatch'; +import { InventoryBadge } from './InventoryBadge'; +import { MaterialBadge } from './MaterialBadge'; +import { EnhancedFilters } from './EnhancedFilters'; +import '../styles/select.css'; + +interface FilamentTableV2Props { + filaments: (Filament | FilamentV2)[]; + loading?: boolean; + error?: string; +} + +export const FilamentTableV2: React.FC = ({ filaments, loading, error }) => { + const [searchTerm, setSearchTerm] = useState(''); + const [sortField, setSortField] = useState('color.name'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); + const [filters, setFilters] = useState({ + brand: '', + material: '', + storageCondition: '', + isRefill: null as boolean | null, + color: '' + }); + + // Convert legacy filaments to V2 format for display + const normalizedFilaments = useMemo(() => { + return filaments.map(f => { + if (isFilamentV2(f)) return f; + + // Convert legacy format + const legacy = f as Filament; + const material = { + base: legacy.tip || 'PLA', + modifier: legacy.finish !== 'Basic' ? legacy.finish : undefined + }; + + const storageCondition = legacy.vakum?.toLowerCase().includes('vakuum') ? 'vacuum' : + legacy.otvoreno?.toLowerCase().includes('otvorena') ? 'opened' : 'sealed'; + + return { + id: legacy.id || `legacy-${Math.random().toString(36).substr(2, 9)}`, + brand: legacy.brand, + type: legacy.tip as any || 'PLA', + material, + color: { name: legacy.boja }, + weight: { value: 1000, unit: 'g' as const }, + diameter: 1.75, + inventory: { + total: parseInt(legacy.kolicina) || 1, + available: storageCondition === 'opened' ? 0 : 1, + inUse: 0, + locations: { + vacuum: storageCondition === 'vacuum' ? 1 : 0, + opened: storageCondition === 'opened' ? 1 : 0, + printer: 0 + } + }, + pricing: { + purchasePrice: legacy.cena ? parseFloat(legacy.cena) : undefined, + currency: 'RSD' as const + }, + condition: { + isRefill: legacy.refill === 'Da', + storageCondition: storageCondition as any + }, + tags: [], + createdAt: '', + updatedAt: '' + } as FilamentV2; + }); + }, [filaments]); + + // Get unique values for filters + const uniqueValues = useMemo(() => ({ + brands: [...new Set(normalizedFilaments.map(f => f.brand))].sort(), + materials: [...new Set(normalizedFilaments.map(f => f.material.base))].sort(), + colors: [...new Set(normalizedFilaments.map(f => f.color.name))].sort() + }), [normalizedFilaments]); + + // Filter and sort filaments + const filteredAndSortedFilaments = useMemo(() => { + let filtered = normalizedFilaments.filter(filament => { + // Search filter + const searchLower = searchTerm.toLowerCase(); + const matchesSearch = + filament.brand.toLowerCase().includes(searchLower) || + filament.material.base.toLowerCase().includes(searchLower) || + (filament.material.modifier?.toLowerCase().includes(searchLower)) || + filament.color.name.toLowerCase().includes(searchLower) || + (filament.sku?.toLowerCase().includes(searchLower)); + + // Other filters + const matchesBrand = !filters.brand || filament.brand === filters.brand; + const matchesMaterial = !filters.material || + filament.material.base === filters.material || + `${filament.material.base}-${filament.material.modifier}` === filters.material; + const matchesStorage = !filters.storageCondition || filament.condition.storageCondition === filters.storageCondition; + const matchesRefill = filters.isRefill === null || filament.condition.isRefill === filters.isRefill; + const matchesColor = !filters.color || filament.color.name === filters.color; + + return matchesSearch && matchesBrand && matchesMaterial && matchesStorage && matchesRefill && matchesColor; + }); + + // Sort + filtered.sort((a, b) => { + let aVal: any = a; + let bVal: any = b; + + // Handle nested properties + const fields = sortField.split('.'); + for (const field of fields) { + aVal = aVal?.[field]; + bVal = bVal?.[field]; + } + + if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1; + if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1; + return 0; + }); + + return filtered; + }, [normalizedFilaments, searchTerm, filters, sortField, sortOrder]); + + const handleSort = (field: string) => { + if (sortField === field) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + setSortField(field); + setSortOrder('asc'); + } + }; + + // Inventory summary + const inventorySummary = useMemo(() => { + const summary = { + totalSpools: 0, + availableSpools: 0, + totalWeight: 0, + brandsCount: new Set(), + lowStock: [] as FilamentV2[] + }; + + normalizedFilaments.forEach(f => { + summary.totalSpools += f.inventory.total; + summary.availableSpools += f.inventory.available; + summary.totalWeight += f.inventory.total * f.weight.value; + summary.brandsCount.add(f.brand); + + if (f.inventory.available <= 1 && f.inventory.total > 0) { + summary.lowStock.push(f); + } + }); + + return summary; + }, [normalizedFilaments]); + + if (loading) { + return
Učitavanje...
; + } + + if (error) { + return
{error}
; + } + + return ( +
+ {/* Inventory Summary */} +
+
+
Ukupno kalema
+
{inventorySummary.totalSpools}
+
+
+
Dostupno
+
{inventorySummary.availableSpools}
+
+
+
Ukupna težina
+
{(inventorySummary.totalWeight / 1000).toFixed(1)}kg
+
+
+
Malo na stanju
+
{inventorySummary.lowStock.length}
+
+
+ + {/* Search Bar */} +
+ setSearchTerm(e.target.value)} + className="w-full px-4 py-2 pl-10 pr-4 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:border-blue-500" + /> + + + +
+ + {/* Enhanced Filters */} + + + {/* Table */} +
+ + + + + + + + + + + + + + {filteredAndSortedFilaments.map(filament => ( + + + + + + + + + + ))} + +
handleSort('sku')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"> + SKU + handleSort('brand')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"> + Brend + handleSort('material.base')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"> + Materijal + handleSort('color.name')} className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"> + Boja + + Skladište + + Težina + + Status +
+ {filament.sku || filament.id.substring(0, 8)} + + {filament.brand} + + + + + +
+ {filament.inventory.locations.vacuum > 0 && ( + + )} + {filament.inventory.locations.opened > 0 && ( + + )} + {filament.inventory.locations.printer > 0 && ( + + )} +
+
+ {filament.weight.value}{filament.weight.unit} + +
+ {filament.condition.isRefill && ( + + Punjenje + + )} + {filament.inventory.available === 0 && ( + + Nema na stanju + + )} + {filament.inventory.available === 1 && ( + + Poslednji + + )} +
+
+
+ +
+ Prikazano {filteredAndSortedFilaments.length} od {normalizedFilaments.length} filamenata +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/InventoryBadge.tsx b/src/components/InventoryBadge.tsx new file mode 100644 index 0000000..3809208 --- /dev/null +++ b/src/components/InventoryBadge.tsx @@ -0,0 +1,89 @@ +import React from 'react'; + +interface InventoryBadgeProps { + type: 'vacuum' | 'opened' | 'printer' | 'total' | 'available'; + count: number; + className?: string; +} + +export const InventoryBadge: React.FC = ({ type, count, className = '' }) => { + if (count === 0) return null; + + const getIcon = () => { + switch (type) { + case 'vacuum': + return ( + + + + ); + case 'opened': + return ( + + + + ); + case 'printer': + return ( + + + + ); + case 'total': + return ( + + + + ); + case 'available': + return ( + + + + ); + } + }; + + const getColor = () => { + switch (type) { + case 'vacuum': + return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'; + case 'opened': + return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'; + case 'printer': + return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'; + case 'total': + return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'; + case 'available': + return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'; + } + }; + + const getLabel = () => { + switch (type) { + case 'vacuum': + return 'Vakuum'; + case 'opened': + return 'Otvoreno'; + case 'printer': + return 'U printeru'; + case 'total': + return 'Ukupno'; + case 'available': + return 'Dostupno'; + } + }; + + return ( + + {getIcon()} + {count} + {getLabel()} + + ); +}; \ No newline at end of file diff --git a/src/components/MaterialBadge.tsx b/src/components/MaterialBadge.tsx new file mode 100644 index 0000000..01581e8 --- /dev/null +++ b/src/components/MaterialBadge.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +interface MaterialBadgeProps { + base: string; + modifier?: string; + className?: string; +} + +export const MaterialBadge: React.FC = ({ base, modifier, className = '' }) => { + const getBaseColor = () => { + switch (base) { + case 'PLA': + return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'; + case 'PETG': + return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'; + case 'ABS': + return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'; + case 'TPU': + return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'; + default: + return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'; + } + }; + + const getModifierIcon = () => { + switch (modifier) { + case 'Silk': + return '✨'; + case 'Matte': + return '🔵'; + case 'Glow': + return '💡'; + case 'Wood': + return '🪵'; + case 'CF': + return '⚫'; + default: + return null; + } + }; + + return ( +
+ + {base} + + {modifier && ( + + {getModifierIcon()} {modifier} + + )} +
+ ); +}; \ No newline at end of file diff --git a/src/server/confluence.ts b/src/server/confluence.ts deleted file mode 100644 index fd7c096..0000000 --- a/src/server/confluence.ts +++ /dev/null @@ -1,176 +0,0 @@ -import axios from 'axios'; -import * as cheerio from 'cheerio'; - -export interface Filament { - brand: string; - tip: string; - finish: string; - boja: string; - refill: string; - vakum: string; - otvoreno: string; - kolicina: string; - cena: string; -} - -const mockFilaments: Filament[] = [ - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Mistletoe Green", refill: "", vakum: "vakuum x1", otvoreno: "otvorena x1", kolicina: "2", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Indigo Purple", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Black", refill: "", vakum: "", otvoreno: "2x otvorena", kolicina: "2", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Black", refill: "Da", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Jade White", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Gray", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Red", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Hot Pink", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Cocoa Brown", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "White", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Cotton Candy Cloud", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Sunflower Yellow", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Yellow", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Magenta", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Beige", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Basic", boja: "Cyan", refill: "", vakum: "vakuum", otvoreno: "", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Scarlet Red", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Mandarin Orange", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Marine Blue", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Charcoal", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" }, - { brand: "BambuLab", tip: "PLA", finish: "Matte", boja: "Ivory White", refill: "", vakum: "", otvoreno: "otvorena", kolicina: "", cena: "" } -]; - -export async function fetchFromConfluence(env: any): Promise { - const confluenceUrl = env.CONFLUENCE_API_URL; - const confluenceToken = env.CONFLUENCE_TOKEN; - const pageId = env.CONFLUENCE_PAGE_ID; - - console.log('Confluence config:', { - url: confluenceUrl ? 'Set' : 'Missing', - token: confluenceToken ? 'Set' : 'Missing', - pageId: pageId || 'Missing' - }); - - if (!confluenceUrl || !confluenceToken || !pageId) { - console.warn('Confluence configuration missing, using mock data'); - return mockFilaments; - } - - try { - console.log('Fetching from Confluence:', `${confluenceUrl}/wiki/rest/api/content/${pageId}`); - - // Create Basic auth token from email and API token - const auth = Buffer.from(`dax@demirix.com:${confluenceToken}`).toString('base64'); - - const response = await axios.get( - `${confluenceUrl}/wiki/rest/api/content/${pageId}?expand=body.storage`, - { - headers: { - 'Authorization': `Basic ${auth}`, - 'Accept': 'application/json' - } - } - ); - - console.log('Response status:', response.status); - const htmlContent = response.data.body?.storage?.value || ''; - - if (!htmlContent) { - console.error('No HTML content in response'); - throw new Error('No content found'); - } - - const filaments = parseConfluenceTable(htmlContent); - - // Always return parsed data, never fall back to mock - console.log(`Returning ${filaments.length} filaments from Confluence`); - return filaments; - } catch (error) { - console.error('Failed to fetch from Confluence:', error); - if (axios.isAxiosError(error)) { - console.error('Response:', error.response?.status, error.response?.data); - } - throw error; // Don't return mock data - } -} - -function parseConfluenceTable(html: string): Filament[] { - const $ = cheerio.load(html); - const filaments: Filament[] = []; - - console.log('HTML length:', html.length); - console.log('Number of tables found:', $('table').length); - - // Find all tables and process each one - $('table').each((tableIndex, table) => { - let headers: string[] = []; - - // Get headers - $(table).find('tr').first().find('th, td').each((_i, cell) => { - headers.push($(cell).text().trim()); - }); - - console.log(`Table ${tableIndex} headers:`, headers); - - // Skip if not our filament table (check for expected headers) - if (!headers.includes('Boja') || !headers.includes('Brand')) { - console.log(`Skipping table ${tableIndex} - missing required headers`); - return; - } - - // Process rows - $(table).find('tr').slice(1).each((_rowIndex, row) => { - const cells = $(row).find('td'); - if (cells.length >= headers.length) { - const filament: any = {}; - - cells.each((cellIndex, cell) => { - const headerName = headers[cellIndex]; - const cellText = $(cell).text().trim(); - - // Debug log for the problematic column - if (cellIndex === 7) { // Količina should be the 8th column (index 7) - console.log(`Column 7 - Header: "${headerName}", Value: "${cellText}"`); - } - - // Map headers to our expected structure - switch(headerName.toLowerCase()) { - case 'brand': - filament.brand = cellText; - break; - case 'tip': - filament.tip = cellText; - break; - case 'finish': - filament.finish = cellText; - break; - case 'boja': - filament.boja = cellText; - break; - case 'refill': - filament.refill = cellText; - break; - case 'vakum': - filament.vakum = cellText; - break; - case 'otvoreno': - filament.otvoreno = cellText; - break; - case 'količina': - case 'kolicina': // Handle possible typo - filament.kolicina = cellText; - break; - case 'cena': - filament.cena = cellText; - break; - } - }); - - // Only add if we have the required fields - if (filament.brand && filament.boja) { - filaments.push(filament as Filament); - } - } - }); - }); - - console.log(`Parsed ${filaments.length} filaments from Confluence`); - return filaments; // Return whatever we found, don't fall back to mock -} \ No newline at end of file diff --git a/src/types/filament.ts b/src/types/filament.ts index b46370f..a50f5fe 100644 --- a/src/types/filament.ts +++ b/src/types/filament.ts @@ -1,4 +1,5 @@ export interface Filament { + id?: string; brand: string; tip: string; finish: string; @@ -8,4 +9,7 @@ export interface Filament { otvoreno: string; kolicina: string; cena: string; + status?: string; + createdAt?: string; + updatedAt?: string; } \ No newline at end of file diff --git a/src/types/filament.v2.ts b/src/types/filament.v2.ts new file mode 100644 index 0000000..0c5f4d1 --- /dev/null +++ b/src/types/filament.v2.ts @@ -0,0 +1,142 @@ +// Version 2 - Improved filament data structure + +export type MaterialBase = 'PLA' | 'PETG' | 'ABS' | 'TPU' | 'SILK' | 'CF' | 'WOOD'; +export type MaterialModifier = 'Silk' | 'Matte' | 'Glow' | 'Wood' | 'CF'; +export type StorageCondition = 'vacuum' | 'sealed' | 'opened' | 'desiccant'; +export type Currency = 'RSD' | 'EUR' | 'USD'; + +export interface Material { + base: MaterialBase; + modifier?: MaterialModifier; +} + +export interface Color { + name: string; + hex?: string; + pantone?: string; +} + +export interface Weight { + value: number; + unit: 'g' | 'kg'; +} + +export interface InventoryLocation { + vacuum: number; + opened: number; + printer: number; +} + +export interface Inventory { + total: number; + available: number; + inUse: number; + locations: InventoryLocation; +} + +export interface Pricing { + purchasePrice?: number; + currency: Currency; + supplier?: string; + purchaseDate?: string; +} + +export interface Condition { + isRefill: boolean; + openedDate?: string; + expiryDate?: string; + storageCondition: StorageCondition; + humidity?: number; +} + +// Legacy fields for backwards compatibility +export interface LegacyFields { + tip: string; + finish: string; + boja: string; + refill: string; + vakum: string; + otvoreno: string; + kolicina: string; + cena: string; + status?: string; +} + +export interface FilamentV2 { + // Identifiers + id: string; + sku?: string; + + // Product Info + brand: string; + type: MaterialBase; + material: Material; + color: Color; + + // Physical Properties + weight: Weight; + diameter: number; + + // Inventory Status + inventory: Inventory; + + // Purchase Info + pricing: Pricing; + + // Condition + condition: Condition; + + // Metadata + tags: string[]; + notes?: string; + images?: string[]; + + // Timestamps + createdAt: string; + updatedAt: string; + lastUsed?: string; + + // Backwards compatibility + _legacy?: LegacyFields; +} + +// Helper type guards +export const isFilamentV2 = (filament: any): filament is FilamentV2 => { + return filament && + typeof filament === 'object' && + 'material' in filament && + 'inventory' in filament && + 'condition' in filament; +}; + +// Utility functions +export const getTotalWeight = (filament: FilamentV2): number => { + const multiplier = filament.weight.unit === 'kg' ? 1000 : 1; + return filament.inventory.total * filament.weight.value * multiplier; +}; + +export const getAvailableWeight = (filament: FilamentV2): number => { + const multiplier = filament.weight.unit === 'kg' ? 1000 : 1; + return filament.inventory.available * filament.weight.value * multiplier; +}; + +export const isLowStock = (filament: FilamentV2, threshold = 1): boolean => { + return filament.inventory.available <= threshold && filament.inventory.total > 0; +}; + +export const needsRefill = (filament: FilamentV2): boolean => { + return filament.inventory.available === 0 && filament.inventory.total > 0; +}; + +export const isExpired = (filament: FilamentV2): boolean => { + if (!filament.condition.expiryDate) return false; + return new Date(filament.condition.expiryDate) < new Date(); +}; + +export const daysOpen = (filament: FilamentV2): number | null => { + if (!filament.condition.openedDate) return null; + const opened = new Date(filament.condition.openedDate); + const now = new Date(); + const diff = now.getTime() - opened.getTime(); + return Math.floor(diff / (1000 * 60 * 60 * 24)); +}; \ No newline at end of file diff --git a/terraform/environments/dev/terraform.tfvars b/terraform/environments/dev/terraform.tfvars new file mode 100644 index 0000000..39a2018 --- /dev/null +++ b/terraform/environments/dev/terraform.tfvars @@ -0,0 +1,12 @@ +# Development Environment Variables +environment = "dev" +app_name = "filamenteka" + +# API Gateway stage +api_stage_name = "dev" + +# DynamoDB table +dynamodb_table_name = "filamenteka-filaments-dev" + +# Domain configuration (dev subdomain) +domain_name = "dev.filamenteka.rs" \ No newline at end of file diff --git a/terraform/environments/prod/terraform.tfvars b/terraform/environments/prod/terraform.tfvars new file mode 100644 index 0000000..4167007 --- /dev/null +++ b/terraform/environments/prod/terraform.tfvars @@ -0,0 +1,12 @@ +# Production Environment Variables +environment = "production" +app_name = "filamenteka" + +# API Gateway stage +api_stage_name = "production" + +# DynamoDB table +dynamodb_table_name = "filamenteka-filaments" + +# Domain configuration +domain_name = "filamenteka.rs" \ No newline at end of file diff --git a/terraform/lambda.tf b/terraform/lambda.tf index 665db64..b4b289c 100644 --- a/terraform/lambda.tf +++ b/terraform/lambda.tf @@ -66,7 +66,7 @@ resource "aws_lambda_function" "filaments_api" { environment { variables = { TABLE_NAME = aws_dynamodb_table.filaments.name - CORS_ORIGIN = var.domain_name != "" ? "https://${var.domain_name}" : "*" + CORS_ORIGIN = "*" } } @@ -89,7 +89,7 @@ resource "aws_lambda_function" "auth_api" { JWT_SECRET = var.jwt_secret ADMIN_USERNAME = var.admin_username ADMIN_PASSWORD_HASH = var.admin_password_hash - CORS_ORIGIN = var.domain_name != "" ? "https://${var.domain_name}" : "*" + CORS_ORIGIN = "*" } } diff --git a/terraform/main.tf b/terraform/main.tf index e1af232..8c97ca1 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -49,9 +49,6 @@ resource "aws_amplify_app" "filamenteka" { # Environment variables environment_variables = { - CONFLUENCE_API_URL = var.confluence_api_url - CONFLUENCE_TOKEN = var.confluence_token - CONFLUENCE_PAGE_ID = var.confluence_page_id NEXT_PUBLIC_API_URL = aws_api_gateway_stage.api.invoke_url } diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example index 4801e1c..b610c7c 100644 --- a/terraform/terraform.tfvars.example +++ b/terraform/terraform.tfvars.example @@ -3,10 +3,6 @@ github_repository = "https://github.com/yourusername/filamenteka" github_token = "ghp_your_github_token_here" -confluence_api_url = "https://your-domain.atlassian.net" -confluence_token = "your_confluence_api_token" -confluence_page_id = "your_confluence_page_id" - # Admin Authentication jwt_secret = "your-secret-key-at-least-32-characters-long" admin_username = "admin" diff --git a/terraform/variables.tf b/terraform/variables.tf index fa71af6..d10df58 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -9,22 +9,6 @@ variable "github_token" { sensitive = true } -variable "confluence_api_url" { - description = "Confluence API base URL" - type = string -} - -variable "confluence_token" { - description = "Confluence API token" - type = string - sensitive = true -} - -variable "confluence_page_id" { - description = "Confluence page ID containing the filament table" - type = string -} - variable "domain_name" { description = "Custom domain name (optional)" type = string