Clean up obsolete resources and update documentation
- Remove all DynamoDB-related scripts and data files - Remove AWS SDK dependencies from scripts - Update environment files to remove DynamoDB/Lambda references - Update PROJECT_STRUCTURE.md to reflect current architecture - Clean up Terraform variables and examples - Add PLA Matte to special pricing (3999 RSD / 3499 RSD refill) - Make all table columns sortable - Remove old Terraform state backups - Remove temporary data import files
This commit is contained in:
@@ -4,6 +4,3 @@ NODE_ENV=production
|
|||||||
# API Configuration
|
# API Configuration
|
||||||
NEXT_PUBLIC_API_URL=https://api.filamenteka.rs/api
|
NEXT_PUBLIC_API_URL=https://api.filamenteka.rs/api
|
||||||
|
|
||||||
# AWS Configuration (for reference - no longer used)
|
|
||||||
# AWS_REGION=eu-central-1
|
|
||||||
# DYNAMODB_TABLE_NAME=filamenteka-filaments
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Project Structure
|
# Project Structure
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
Filamenteka is organized with clear separation between environments, infrastructure, and application code.
|
Filamenteka is organized with clear separation between frontend, API, and infrastructure code.
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
@@ -10,48 +10,50 @@ filamenteka/
|
|||||||
├── app/ # Next.js app directory
|
├── app/ # Next.js app directory
|
||||||
│ ├── page.tsx # Main page
|
│ ├── page.tsx # Main page
|
||||||
│ ├── layout.tsx # Root layout
|
│ ├── layout.tsx # Root layout
|
||||||
│ ├── admin/ # Admin pages
|
│ └── upadaj/ # Admin pages
|
||||||
│ │ ├── page.tsx # Admin login
|
│ ├── page.tsx # Admin login
|
||||||
│ │ └── dashboard/ # Admin dashboard
|
│ ├── dashboard/ # Filament management
|
||||||
│ └── globals.css # Global styles
|
│ └── colors/ # Color management
|
||||||
│
|
│
|
||||||
├── src/ # Source code
|
├── src/ # Source code
|
||||||
│ ├── components/ # React components
|
│ ├── components/ # React components
|
||||||
│ │ ├── FilamentTable.tsx
|
│ │ ├── FilamentTableV2.tsx
|
||||||
│ │ ├── FilamentForm.tsx
|
│ │ ├── EnhancedFilters.tsx
|
||||||
│ │ └── ColorCell.tsx
|
│ │ ├── ColorSwatch.tsx
|
||||||
|
│ │ ├── InventoryBadge.tsx
|
||||||
|
│ │ └── MaterialBadge.tsx
|
||||||
│ ├── types/ # TypeScript types
|
│ ├── types/ # TypeScript types
|
||||||
│ │ └── filament.ts
|
│ │ ├── filament.ts
|
||||||
│ ├── data/ # Data and utilities
|
│ │ └── filament.v2.ts
|
||||||
│ │ └── bambuLabColors.ts
|
│ ├── services/ # API services
|
||||||
|
│ │ └── api.ts
|
||||||
│ └── styles/ # Component styles
|
│ └── styles/ # Component styles
|
||||||
|
│ ├── index.css
|
||||||
│ └── select.css
|
│ └── select.css
|
||||||
│
|
│
|
||||||
├── lambda/ # AWS Lambda functions
|
├── api/ # Node.js Express API
|
||||||
│ ├── filaments/ # Filaments CRUD API
|
│ ├── server.js # Express server
|
||||||
│ │ ├── index.js
|
│ ├── migrate.js # Database migration script
|
||||||
│ │ └── package.json
|
│ ├── package.json # API dependencies
|
||||||
│ └── auth/ # Authentication API
|
│ └── Dockerfile # Docker configuration
|
||||||
│ ├── index.js
|
│
|
||||||
│ └── package.json
|
├── database/ # Database schemas
|
||||||
|
│ └── schema.sql # PostgreSQL schema
|
||||||
│
|
│
|
||||||
├── terraform/ # Infrastructure as Code
|
├── terraform/ # Infrastructure as Code
|
||||||
│ ├── environments/ # Environment-specific configs
|
│ ├── main.tf # Main configuration
|
||||||
│ │ ├── dev/
|
│ ├── vpc.tf # VPC and networking
|
||||||
│ │ └── prod/
|
│ ├── rds.tf # PostgreSQL RDS
|
||||||
│ ├── main.tf # Main Terraform configuration
|
│ ├── ec2-api.tf # EC2 for API server
|
||||||
│ ├── dynamodb.tf # DynamoDB tables
|
│ ├── alb.tf # Application Load Balancer
|
||||||
│ ├── lambda.tf # Lambda functions
|
│ ├── ecr.tf # Docker registry
|
||||||
│ ├── api_gateway.tf # API Gateway
|
│ ├── cloudflare-api.tf # Cloudflare DNS
|
||||||
│ └── variables.tf # Variable definitions
|
│ └── variables.tf # Variable definitions
|
||||||
│
|
│
|
||||||
├── scripts/ # Utility scripts
|
├── scripts/ # Utility scripts
|
||||||
│ ├── data-import/ # Data import tools
|
|
||||||
│ │ ├── import-pdf-data.js
|
|
||||||
│ │ └── clear-dynamo.js
|
|
||||||
│ ├── security/ # Security checks
|
│ ├── security/ # Security checks
|
||||||
│ │ └── security-check.js
|
│ │ └── security-check.js
|
||||||
│ └── build/ # Build scripts
|
│ └── pre-commit.sh # Git pre-commit hook
|
||||||
│
|
│
|
||||||
├── config/ # Configuration files
|
├── config/ # Configuration files
|
||||||
│ └── environments.js # Environment configuration
|
│ └── environments.js # Environment configuration
|
||||||
@@ -64,23 +66,25 @@ filamenteka/
|
|||||||
- `.env.development` - Development environment variables
|
- `.env.development` - Development environment variables
|
||||||
- `.env.production` - Production environment variables
|
- `.env.production` - Production environment variables
|
||||||
- `.env.local` - Local overrides (not committed)
|
- `.env.local` - Local overrides (not committed)
|
||||||
- `.env.development.local` - Local dev overrides (not committed)
|
|
||||||
|
|
||||||
## Key Concepts
|
## Key Concepts
|
||||||
|
|
||||||
### Environments
|
### Architecture
|
||||||
- **Development**: Uses `dev` API Gateway stage and separate DynamoDB table
|
- **Frontend**: Next.js static site hosted on AWS Amplify
|
||||||
- **Production**: Uses `production` API Gateway stage and main DynamoDB table
|
- **API**: Node.js Express server running on EC2
|
||||||
|
- **Database**: PostgreSQL on AWS RDS
|
||||||
|
- **HTTPS**: Application Load Balancer with ACM certificate
|
||||||
|
|
||||||
### Data Flow
|
### Data Flow
|
||||||
1. Frontend (Next.js) → API Gateway → Lambda Functions → DynamoDB
|
1. Frontend (Next.js) → HTTPS API (ALB) → Express Server (EC2) → PostgreSQL (RDS)
|
||||||
2. Authentication via JWT tokens stored in localStorage
|
2. Authentication via JWT tokens
|
||||||
3. Real-time data updates every 5 minutes
|
3. Real-time database synchronization
|
||||||
|
|
||||||
### Infrastructure
|
### Infrastructure
|
||||||
- Managed via Terraform
|
- Managed via Terraform
|
||||||
- Separate resources for dev/prod
|
- AWS services: RDS, EC2, ALB, VPC, ECR, Amplify
|
||||||
- AWS services: DynamoDB, Lambda, API Gateway, Amplify
|
- Cloudflare for DNS management
|
||||||
|
- Docker for API containerization
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
@@ -89,28 +93,27 @@ filamenteka/
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Deploy to Dev**
|
2. **Deploy Infrastructure**
|
||||||
```bash
|
```bash
|
||||||
cd terraform
|
cd terraform
|
||||||
terraform apply -var-file=environments/dev/terraform.tfvars
|
terraform apply
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Deploy to Production**
|
3. **Deploy API Updates**
|
||||||
|
- API automatically pulls latest Docker image every 5 minutes
|
||||||
|
- Or manually: SSH to EC2 and run deployment script
|
||||||
|
|
||||||
|
## Database Management
|
||||||
|
|
||||||
|
### Run Migrations
|
||||||
```bash
|
```bash
|
||||||
cd terraform
|
cd api
|
||||||
terraform apply -var-file=environments/prod/terraform.tfvars
|
npm run migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data Management
|
### Connect to Database
|
||||||
|
|
||||||
### Import Data from PDF
|
|
||||||
```bash
|
```bash
|
||||||
node scripts/data-import/import-pdf-data.js
|
psql postgresql://user:pass@rds-endpoint/filamenteka
|
||||||
```
|
|
||||||
|
|
||||||
### Clear DynamoDB Table
|
|
||||||
```bash
|
|
||||||
node scripts/data-import/clear-dynamo.js
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
@@ -119,3 +122,5 @@ node scripts/data-import/clear-dynamo.js
|
|||||||
- JWT authentication for admin
|
- JWT authentication for admin
|
||||||
- Environment-specific configurations
|
- Environment-specific configurations
|
||||||
- Pre-commit security checks
|
- Pre-commit security checks
|
||||||
|
- HTTPS everywhere
|
||||||
|
- VPC isolation for backend services
|
||||||
@@ -2,15 +2,11 @@
|
|||||||
const environments = {
|
const environments = {
|
||||||
development: {
|
development: {
|
||||||
name: 'development',
|
name: 'development',
|
||||||
apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api',
|
apiUrl: process.env.NEXT_PUBLIC_API_URL || 'https://api.filamenteka.rs/api'
|
||||||
dynamoTableName: 'filamenteka-filaments-dev',
|
|
||||||
awsRegion: 'eu-central-1'
|
|
||||||
},
|
},
|
||||||
production: {
|
production: {
|
||||||
name: 'production',
|
name: 'production',
|
||||||
apiUrl: process.env.NEXT_PUBLIC_API_URL,
|
apiUrl: process.env.NEXT_PUBLIC_API_URL
|
||||||
dynamoTableName: 'filamenteka-filaments',
|
|
||||||
awsRegion: 'eu-central-1'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
422
data.json
422
data.json
@@ -1,422 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Mistletoe Green",
|
|
||||||
"boja_hex": "#3a5a40",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 2,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Indingo Purple",
|
|
||||||
"boja_hex": "#4b0082",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Black",
|
|
||||||
"boja_hex": "#000000",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 2,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Black",
|
|
||||||
"boja_hex": "#000000",
|
|
||||||
"refill": "Da",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Jade White",
|
|
||||||
"boja_hex": "#f0f8ff",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Gray",
|
|
||||||
"boja_hex": "#808080",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Red",
|
|
||||||
"boja_hex": "#ff0000",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Hot Pink",
|
|
||||||
"boja_hex": "#ff69b4",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Cocoa Brown",
|
|
||||||
"boja_hex": "#d2691e",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "White",
|
|
||||||
"boja_hex": "#ffffff",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Coton Candy Cloud",
|
|
||||||
"boja_hex": "#ffb6c1",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Sunflower Yellow",
|
|
||||||
"boja_hex": "#ffda03",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Yellow",
|
|
||||||
"boja_hex": "#ffff00",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Magenta",
|
|
||||||
"boja_hex": "#ff00ff",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Beige",
|
|
||||||
"boja_hex": "#f5f5dc",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Cyan",
|
|
||||||
"boja_hex": "#00ffff",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Matte",
|
|
||||||
"boja": "Scarlet Red",
|
|
||||||
"boja_hex": "#ff2400",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Matte",
|
|
||||||
"boja": "Mandarin Orange",
|
|
||||||
"boja_hex": "#ff8c00",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Matte",
|
|
||||||
"boja": "Marine Blue",
|
|
||||||
"boja_hex": "#000080",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Matte",
|
|
||||||
"boja": "Charcoal",
|
|
||||||
"boja_hex": "#36454f",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Matte",
|
|
||||||
"boja": "Ivory White",
|
|
||||||
"boja_hex": "#fffff0",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Matte",
|
|
||||||
"boja": "Ivory White",
|
|
||||||
"boja_hex": "#fffff0",
|
|
||||||
"refill": "Da",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Matte",
|
|
||||||
"boja": "Ash Gray",
|
|
||||||
"boja_hex": "#b2beb5",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Cobalt Blue",
|
|
||||||
"boja_hex": "#0047ab",
|
|
||||||
"refill": "Da",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Turquoise",
|
|
||||||
"boja_hex": "#40e0d0",
|
|
||||||
"refill": "Da",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Matte",
|
|
||||||
"boja": "Nardo Gray",
|
|
||||||
"boja_hex": "#4d4d4d",
|
|
||||||
"refill": "Da",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Bright Green",
|
|
||||||
"boja_hex": "#66ff00",
|
|
||||||
"refill": "Da",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Matte",
|
|
||||||
"boja": "Charcoal",
|
|
||||||
"boja_hex": "#36454f",
|
|
||||||
"refill": "Da",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Gold",
|
|
||||||
"boja_hex": "#ffd700",
|
|
||||||
"refill": "Da",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Glow",
|
|
||||||
"boja": "Glow Green",
|
|
||||||
"boja_hex": "#39ff14",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "Wood",
|
|
||||||
"boja": "Black Walnut",
|
|
||||||
"boja_hex": "#5d4e37",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "CF",
|
|
||||||
"boja": "Black",
|
|
||||||
"boja_hex": "#000000",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PLA",
|
|
||||||
"finish": "CF",
|
|
||||||
"boja": "Jeans Blue",
|
|
||||||
"boja_hex": "#5dadec",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Ne",
|
|
||||||
"otvoreno": "Da",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "PETG",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Black",
|
|
||||||
"boja_hex": "#000000",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"brand": "BambuLab",
|
|
||||||
"tip": "ABS",
|
|
||||||
"finish": "Basic",
|
|
||||||
"boja": "Black",
|
|
||||||
"boja_hex": "#000000",
|
|
||||||
"refill": "Ne",
|
|
||||||
"vakum": "Da",
|
|
||||||
"otvoreno": "Ne",
|
|
||||||
"kolicina": 1,
|
|
||||||
"cena": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
141
import-data.js
141
import-data.js
@@ -1,141 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
const fs = require('fs');
|
|
||||||
const https = require('https');
|
|
||||||
|
|
||||||
// Read the data file
|
|
||||||
const data = JSON.parse(fs.readFileSync('./data.json', 'utf8'));
|
|
||||||
const uniqueColors = JSON.parse(fs.readFileSync('./unique_colors.json', 'utf8'));
|
|
||||||
|
|
||||||
const API_URL = 'https://api.filamenteka.rs/api';
|
|
||||||
|
|
||||||
// First, get auth token
|
|
||||||
async function getAuthToken() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const postData = JSON.stringify({
|
|
||||||
username: process.env.ADMIN_USERNAME || 'admin',
|
|
||||||
password: process.env.ADMIN_PASSWORD || 'admin'
|
|
||||||
});
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
hostname: 'api.filamenteka.rs',
|
|
||||||
path: '/api/login',
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Content-Length': postData.length
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const req = https.request(options, (res) => {
|
|
||||||
let data = '';
|
|
||||||
res.on('data', (chunk) => data += chunk);
|
|
||||||
res.on('end', () => {
|
|
||||||
try {
|
|
||||||
const response = JSON.parse(data);
|
|
||||||
resolve(response.token);
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', reject);
|
|
||||||
req.write(postData);
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make authenticated request
|
|
||||||
async function makeRequest(method, path, data, token) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const postData = data ? JSON.stringify(data) : '';
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
hostname: 'api.filamenteka.rs',
|
|
||||||
path: `/api${path}`,
|
|
||||||
method: method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (postData) {
|
|
||||||
options.headers['Content-Length'] = postData.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
const req = https.request(options, (res) => {
|
|
||||||
let responseData = '';
|
|
||||||
res.on('data', (chunk) => responseData += chunk);
|
|
||||||
res.on('end', () => {
|
|
||||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
||||||
try {
|
|
||||||
resolve(responseData ? JSON.parse(responseData) : null);
|
|
||||||
} catch {
|
|
||||||
resolve(responseData);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reject(new Error(`HTTP ${res.statusCode}: ${responseData}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', reject);
|
|
||||||
if (postData) req.write(postData);
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function importData() {
|
|
||||||
try {
|
|
||||||
console.log('Getting auth token...');
|
|
||||||
const token = await getAuthToken();
|
|
||||||
console.log('Authentication successful!');
|
|
||||||
|
|
||||||
// Import colors first
|
|
||||||
console.log('\nImporting colors...');
|
|
||||||
for (const color of uniqueColors) {
|
|
||||||
try {
|
|
||||||
await makeRequest('POST', '/colors', {
|
|
||||||
name: color.name,
|
|
||||||
hex: color.hex
|
|
||||||
}, token);
|
|
||||||
console.log(`✓ Added color: ${color.name}`);
|
|
||||||
} catch (err) {
|
|
||||||
if (err.message.includes('already exists')) {
|
|
||||||
console.log(`⚠ Color already exists: ${color.name}`);
|
|
||||||
} else {
|
|
||||||
console.error(`✗ Failed to add color ${color.name}:`, err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import filaments
|
|
||||||
console.log('\nImporting filaments...');
|
|
||||||
for (const filament of data) {
|
|
||||||
try {
|
|
||||||
await makeRequest('POST', '/filaments', {
|
|
||||||
brand: filament.brand,
|
|
||||||
tip: filament.tip,
|
|
||||||
finish: filament.finish,
|
|
||||||
boja: filament.boja,
|
|
||||||
bojaHex: filament.boja_hex,
|
|
||||||
refill: filament.refill,
|
|
||||||
vakum: filament.vakum,
|
|
||||||
otvoreno: filament.otvoreno,
|
|
||||||
kolicina: filament.kolicina.toString(),
|
|
||||||
cena: filament.cena.toString()
|
|
||||||
}, token);
|
|
||||||
console.log(`✓ Added filament: ${filament.brand} ${filament.tip} ${filament.finish} - ${filament.boja}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`✗ Failed to add filament:`, err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\nData import completed!');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Import failed:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
importData();
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
#!/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();
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
#!/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();
|
|
||||||
}
|
|
||||||
@@ -1,343 +0,0 @@
|
|||||||
#!/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();
|
|
||||||
}
|
|
||||||
1019
scripts/package-lock.json
generated
1019
scripts/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "filamenteka-scripts",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Migration and utility scripts for Filamenteka",
|
|
||||||
"scripts": {
|
|
||||||
"migrate": "node migrate-with-parser.js",
|
|
||||||
"migrate:clear": "node migrate-with-parser.js --clear"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"aws-sdk": "^2.1472.0",
|
|
||||||
"axios": "^1.6.2",
|
|
||||||
"cheerio": "^1.0.0-rc.12",
|
|
||||||
"dotenv": "^16.3.1",
|
|
||||||
"uuid": "^9.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -250,16 +250,16 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
|||||||
<th onClick={() => 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">
|
<th onClick={() => 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
|
Boja
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
<th onClick={() => handleSort('inventory.total')} 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">
|
||||||
Stanje
|
Stanje
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
<th onClick={() => handleSort('weight.value')} 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">
|
||||||
Težina
|
Težina
|
||||||
</th>
|
</th>
|
||||||
<th onClick={() => handleSort('pricing.purchasePrice')} 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">
|
<th onClick={() => handleSort('pricing.purchasePrice')} 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">
|
||||||
Cena
|
Cena
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
<th onClick={() => handleSort('condition.isRefill')} 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">
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -294,8 +294,8 @@ export const FilamentTableV2: React.FC<FilamentTableV2Props> = ({ filaments, loa
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{(() => {
|
{(() => {
|
||||||
// PLA Basic pricing logic
|
// PLA Basic and Matte pricing logic
|
||||||
if (filament.material.base === 'PLA' && !filament.material.modifier) {
|
if (filament.material.base === 'PLA' && (!filament.material.modifier || filament.material.modifier === 'Matte')) {
|
||||||
if (filament.condition.isRefill && filament.condition.storageCondition !== 'opened') {
|
if (filament.condition.isRefill && filament.condition.storageCondition !== 'opened') {
|
||||||
return '3.499 RSD';
|
return '3.499 RSD';
|
||||||
} else if (!filament.condition.isRefill && filament.condition.storageCondition === 'vacuum') {
|
} else if (!filament.condition.isRefill && filament.condition.storageCondition === 'vacuum') {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
# Copy this file to terraform.tfvars and fill in your values
|
# Copy this file to terraform.tfvars and fill in your values
|
||||||
|
|
||||||
|
# GitHub repository for Amplify
|
||||||
github_repository = "https://github.com/yourusername/filamenteka"
|
github_repository = "https://github.com/yourusername/filamenteka"
|
||||||
github_token = "ghp_your_github_token_here"
|
github_token = "ghp_your_github_token_here"
|
||||||
|
|
||||||
# Admin Authentication
|
# Domain configuration
|
||||||
jwt_secret = "your-secret-key-at-least-32-characters-long"
|
domain_name = "filamenteka.yourdomain.com"
|
||||||
admin_username = "admin"
|
|
||||||
admin_password_hash = "bcrypt-hash-generated-by-generate-password-hash.js"
|
|
||||||
|
|
||||||
# Optional: Custom domain
|
# Cloudflare configuration (optional)
|
||||||
# domain_name = "filamenteka.yourdomain.com"
|
cloudflare_api_token = "your-cloudflare-api-token"
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"name": "Sunflower Yellow",
|
|
||||||
"hex": "#ffda03"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hot Pink",
|
|
||||||
"hex": "#ff69b4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Indingo Purple",
|
|
||||||
"hex": "#4b0082"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Coton Candy Cloud",
|
|
||||||
"hex": "#ffb6c1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Nardo Gray",
|
|
||||||
"hex": "#4d4d4d"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Bright Green",
|
|
||||||
"hex": "#66ff00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Glow Green",
|
|
||||||
"hex": "#39ff14"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Cocoa Brown",
|
|
||||||
"hex": "#d2691e"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "White",
|
|
||||||
"hex": "#ffffff"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Beige",
|
|
||||||
"hex": "#f5f5dc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Gold",
|
|
||||||
"hex": "#ffd700"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Black",
|
|
||||||
"hex": "#000000"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Cyan",
|
|
||||||
"hex": "#00ffff"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Gray",
|
|
||||||
"hex": "#808080"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Cobalt Blue",
|
|
||||||
"hex": "#0047ab"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Magenta",
|
|
||||||
"hex": "#ff00ff"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Charcoal",
|
|
||||||
"hex": "#36454f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Mistletoe Green",
|
|
||||||
"hex": "#3a5a40"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Black Walnut",
|
|
||||||
"hex": "#5d4e37"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Jeans Blue",
|
|
||||||
"hex": "#5dadec"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Mandarin Orange",
|
|
||||||
"hex": "#ff8c00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Ash Gray",
|
|
||||||
"hex": "#b2beb5"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Ivory White",
|
|
||||||
"hex": "#fffff0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Red",
|
|
||||||
"hex": "#ff0000"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Turquoise",
|
|
||||||
"hex": "#40e0d0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Scarlet Red",
|
|
||||||
"hex": "#ff2400"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Marine Blue",
|
|
||||||
"hex": "#000080"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Jade White",
|
|
||||||
"hex": "#f0f8ff"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Yellow",
|
|
||||||
"hex": "#ffff00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user